SpringBoot Security+JWT簡(jiǎn)單搭建的實(shí)現(xiàn)示例
SpringBoot Security
是Spring官方提供的一個(gè)安全框架,他的核心功能是對(duì)系統(tǒng)用戶進(jìn)行認(rèn)證和鑒權(quán),也經(jīng)常在項(xiàng)目中被使用到,本文不介紹其太過深入的內(nèi)容,只介紹如何實(shí)現(xiàn)并完成認(rèn)證和鑒權(quán)的測(cè)試。主要分三步來實(shí)現(xiàn):
- 配置JWT
- 配置Security
- 編寫測(cè)試相關(guān)代碼
首先創(chuàng)建一個(gè)springboot項(xiàng)目,我的版本是2.6.13
,依然是java8
,整合Security+JWT需要用到的Maven
依賴如下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--jwt--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
配置JWT
先在yml
配置文件中添加jwt
相關(guān)配置
jwt: expiration: 3600000 //token過期時(shí)間,1個(gè)小時(shí) tokenHeader: Authorization //token在header中的屬性名 secret: jwt-token-secret //生成token的密鑰
創(chuàng)建jwt
工具類,方便實(shí)現(xiàn)根據(jù)用戶信息生成token
,以及通過token
中獲取用戶信息
@Component @Data public class JwtTokenUtil implements Serializable { private static final long serialVersionUID = -3301605591108950415L; @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; private Clock clock = DefaultClock.INSTANCE; //根據(jù)用戶信息生成token public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); return doGenerateToken(claims, userDetails.getUsername()); } private String doGenerateToken(Map<String, Object> claims, String subject) { final Date createdDate = clock.now(); final Date expirationDate = calculateExpirationDate(createdDate); return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(createdDate) .setExpiration(expirationDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } private Date calculateExpirationDate(Date createdDate) { return new Date(createdDate.getTime() + expiration); } public Boolean validateToken(String token, UserDetails userDetails) { SecurityUserDetails user = (SecurityUserDetails) userDetails; final String username = getUsernameFromToken(token); return (username.equals(user.getUsername()) && !isTokenExpired(token) ); } //通過token獲取用戶名username public String getUsernameFromToken(String token) { return getClaimFromToken(token, Claims::getSubject); } public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) { final Claims claims = getAllClaimsFromToken(token); return claimsResolver.apply(claims); } private Claims getAllClaimsFromToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } private Boolean isTokenExpired(String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(clock.now()); } public Date getExpirationDateFromToken(String token) { return getClaimFromToken(token, Claims::getExpiration); } }
配置Security
編寫一個(gè)存儲(chǔ)用戶信息的UserDetails
的實(shí)現(xiàn)類
@Data public class SysUser { private Integer id; private String username; private String password; }
@Data @EqualsAndHashCode @Accessors(chain = true) //實(shí)現(xiàn)鏈?zhǔn)絪et方法 public class SecurityUserDetails extends SysUser implements UserDetails { //權(quán)限列表 private Collection<? extends GrantedAuthority> authorities; public SecurityUserDetails(String userName,Collection<? extends GrantedAuthority> authorities){ this.setUsername(userName); String encode = new BCryptPasswordEncoder().encode("123456"); this.setPassword(encode); this.setAuthorities(authorities); } /** * 下面這些都返回true * @return */ @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
提示
因?yàn)橹皇怯涗浺幌氯绾螌?shí)現(xiàn)security+jwt,所以沒有從數(shù)據(jù)庫(kù)中讀取真實(shí)的用戶信息,而是直接將用戶信息和權(quán)限信息寫死測(cè)試。
重寫UserDetailsService
的loadUserByUsername
方法實(shí)現(xiàn)具體的認(rèn)證授權(quán)邏輯
@Service public class JwtUserDetailsServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { List<GrantedAuthority> authorityList = new ArrayList<>(); authorityList.add(new SimpleGrantedAuthority("ROLE_USER")); return new SecurityUserDetails(username,authorityList); } }
提示
這里直接把用戶的權(quán)限寫死,ROLE_USER表示用戶擁有USER
權(quán)限,因?yàn)闄?quán)限都是以ROLE_
開頭的。
緊接著創(chuàng)建一個(gè)用戶請(qǐng)求的過濾器,用來攔截用戶請(qǐng)求,分析用戶有沒有該請(qǐng)求的權(quán)限
@Component public class JwtAuthorizationTokenFilter extends OncePerRequestFilter { private final UserDetailsService userDetailsService; private final JwtTokenUtil jwtTokenUtil; private final String tokenHeader; public JwtAuthorizationTokenFilter(@Qualifier("jwtUserDetailsServiceImpl") UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, @Value("${jwt.tokenHeader}") String tokenHeader){ this.userDetailsService = userDetailsService; this.jwtTokenUtil = jwtTokenUtil; this.tokenHeader = tokenHeader; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String requestHeader = request.getHeader(this.tokenHeader); String username = null; String authToken = null; if(requestHeader != null && requestHeader.startsWith("Bearer ")){ authToken = requestHeader.substring(7); try { username = jwtTokenUtil.getUsernameFromToken(authToken); }catch (ExpiredJwtException e){ e.printStackTrace(); } } if(username!=null&& SecurityContextHolder.getContext().getAuthentication() == null){ UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if(jwtTokenUtil.validateToken(authToken,userDetails)){ UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request,response); } }
提示Bearer
必須帶空格,第二個(gè)if
判斷就是為了加載到用戶的信息,并且在Security
上下文中存儲(chǔ)用戶及用戶的權(quán)限的信息
實(shí)現(xiàn)AuthenticationEntryPoint
接口的commence
方法,當(dāng)請(qǐng)求沒有攜帶認(rèn)證信息或者說認(rèn)證失敗時(shí),使用我們自己編寫的處理邏輯。
@Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED); } }
提示
如果請(qǐng)求沒有攜帶認(rèn)證信息或者說認(rèn)證失敗時(shí),會(huì)返回給客戶端401
,如果不重寫commence
方法,默認(rèn)返回403
接下來編寫Security
的核心配置類,重寫WebSecurityConfigurerAdapter
中的configure
方法
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Autowired JwtUserDetailsServiceImpl jwtUserDetailsService; @Autowired JwtAuthorizationTokenFilter authenticationTokenFilter; @Autowired @Lazy PasswordEncoder passwordEncoder; @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint) .and() .authorizeRequests() .antMatchers("/login").permitAll() .antMatchers(HttpMethod.OPTIONS, "/**").anonymous() .anyRequest().authenticated() //除上面以外的都攔截 .and() .csrf().disable() //禁用security自帶的跨域處理 //讓Security不使用session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } @Bean public PasswordEncoder passwordEncoderBean() { return new BCryptPasswordEncoder(); } /** * 認(rèn)證邏輯配置 */ @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder); } }
提示
上面的代碼中,.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
表示使用自定義的認(rèn)證失敗處理邏輯。并且配置類中,自定義了用戶密碼的加密方式,configureGlobal
方法設(shè)置自定義的loadUserByUsername
方法實(shí)現(xiàn)和校驗(yàn)密碼校驗(yàn)的加密方式。
編寫測(cè)試相關(guān)代碼
編寫一個(gè)不需要認(rèn)證授權(quán)就能訪問的登錄接口/login
@RestController public class LoginController { @Autowired @Qualifier("jwtUserDetailsServiceImpl") private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @PostMapping("/login") public String login(@RequestBody SysUser sysUser, HttpServletRequest request){ final UserDetails userDetails = userDetailsService.loadUserByUsername(sysUser.getUsername()); final String token = jwtTokenUtil.generateToken(userDetails); return token; } }
編寫一個(gè)需要USER
權(quán)限的接口/sys/testUser
@RestController @RequestMapping("/sys") public class SysUserController { @PreAuthorize("hasAnyRole('USER')") @PostMapping(value = "/testUser") public String testNeed() { return "hello world"; } }
測(cè)試
啟動(dòng)SpringBoot項(xiàng)目,對(duì)上面的接口進(jìn)行測(cè)試,首先調(diào)用/login
接口登錄并獲取token
請(qǐng)求成功并獲取到jwt
生成的token
。緊接著調(diào)用需要USER
權(quán)限的/testUser
,請(qǐng)求時(shí)要在請(qǐng)求頭里面攜帶token
請(qǐng)求成功!
現(xiàn)在來測(cè)試一下失敗的情況,不傳token
直接請(qǐng)求
請(qǐng)求失敗,返回401
,表示沒有認(rèn)證。再來測(cè)試一下如果將@PreAuthorize("hasAnyRole('USER')")
中的權(quán)限改為Admin
,然后用剛剛生成的token
去請(qǐng)求
@PreAuthorize("hasAnyRole('Admin')") @PostMapping(value = "/testUser") public String testNeed() { return "hello world"; }
由于token中包含的授權(quán)信息是USER
,所以將@PreAuthorize("hasAnyRole('USER')")
中的USER
改為Admin
后,返回了403
,表示沒有這個(gè)權(quán)限。
到此這篇關(guān)于SpringBoot Security+JWT簡(jiǎn)單搭建的實(shí)現(xiàn)示例的文章就介紹到這了,更多相關(guān)SpringBoot Security JWT搭建內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot整合SpringSecurity和JWT的示例
- SpringBoot集成SpringSecurity和JWT做登陸鑒權(quán)的實(shí)現(xiàn)
- SpringBoot3.0+SpringSecurity6.0+JWT的實(shí)現(xiàn)
- Springboot WebFlux集成Spring Security實(shí)現(xiàn)JWT認(rèn)證的示例
- 詳解SpringBoot+SpringSecurity+jwt整合及初體驗(yàn)
- SpringBoot集成Spring security JWT實(shí)現(xiàn)接口權(quán)限認(rèn)證
- SpringBoot3.x接入Security6.x實(shí)現(xiàn)JWT認(rèn)證的完整步驟
- SpringBoot+SpringSecurity+jwt實(shí)現(xiàn)驗(yàn)證
相關(guān)文章
springboot?使用clickhouse實(shí)時(shí)大數(shù)據(jù)分析引擎(使用方式)
這篇文章主要介紹了springboot?使用clickhouse實(shí)時(shí)大數(shù)據(jù)分析引擎的方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2022-02-02Java利用位運(yùn)算實(shí)現(xiàn)比較兩個(gè)數(shù)的大小
這篇文章主要為大家介紹了,在Java中如何不用任何比較判斷符(>,==,<),返回兩個(gè)數(shù)( 32 位整數(shù))中較大的數(shù),感興趣的可以了解一下2022-08-08在java中 利用匿名內(nèi)部類進(jìn)行較簡(jiǎn)潔的雙括弧初始化的方法
本篇文章小編將為大家介紹,關(guān)于在java中 利用匿名內(nèi)部類進(jìn)行較簡(jiǎn)潔的雙括弧初始化的方法,有需要的朋友可以參考一下2013-04-04Spring Data JPA 設(shè)置字段默認(rèn)值方式
這篇文章主要介紹了Spring Data JPA設(shè)置字段默認(rèn)值方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11SpringBoot啟動(dòng)流程SpringApplication準(zhǔn)備階段源碼分析
這篇文章主要為大家介紹了SpringBoot啟動(dòng)流程SpringApplication準(zhǔn)備階段源碼分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04