好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

Springboot集成Spring Security实现JWT认证的步骤详解

1 简介

Spring Security作为成熟且强大的安全框架,得到许多大厂的青睐。而作为前后端分离的SSO方案,JWT也在许多项目中应用。本文将介绍如何通过Spring Security实现JWT认证。

用户与服务器交互大概如下:

客户端获取JWT,一般通过POST方法把用户名/密码传给server; 服务端接收到客户端的请求后,会检验用户名/密码是否正确,如果正确则生成JWT并返回;不正确则返回错误; 客户端拿到JWT后,在有效期内都可以通过JWT来访问资源了,一般把JWT放在请求头;一次获取,多次使用; 服务端校验JWT是否合法,合法则允许客户端正常访问,不合法则返回401。

2 项目整合

我们把要整合的Spring Security和JWT加入到项目的依赖中去:

<dependency> <groupId> org.springframework.boot </groupId> <artifactId> spring-boot-starter-web </artifactId> </dependency> <dependency> <groupId> org.springframework.boot </groupId> <artifactId> spring-boot-starter-security </artifactId> </dependency> <dependency> <groupId> io.jsonwebtoken </groupId> <artifactId> jjwt </artifactId> <version> 0.9.1 </version> </dependency>

2.1 JWT整合

2.1.1 JWT工具类

JWT工具类起码要具有以下功能:

根据用户信息生成JWT; 校验JWT是否合法,如是否被篡改、是否过期等; 从JWT中解析用户信息,如用户名、权限等;

具体代码如下:

@Component public class JwtTokenProvider {   @Autowired JwtProperties jwtProperties ;   @Autowired private CustomUserDetailsService userDetailsService ;   private String secretKey ;   @PostConstruct protected void init () { secretKey = Base64 . getEncoder (). encodeToString ( jwtProperties . getSecretKey (). getBytes ()); }   public String createToken ( String username , List < String > roles ) {   Claims claims = Jwts . claims (). setSubject ( username ); claims . put ( "roles" , roles );   Date now = new Date (); Date validity = new Date ( now . getTime () + jwtProperties . getValidityInMs ());   return Jwts . builder () // . setClaims ( claims ) // . setIssuedAt ( now ) // . setExpiration ( validity ) // . signWith ( SignatureAlgorithm . HS256 , secretKey ) // . compact (); }   public Authentication getAuthentication ( String token ) { UserDetails userDetails = this . userDetailsService . loadUserByUsername ( getUsername ( token )); return new UsernamePasswordAuthenticationToken ( userDetails , "" , userDetails . getAuthorities ()); }   public String getUsername ( String token ) { return Jwts . parser (). setSigningKey ( secretKey ). parseClaimsJws ( token ). getBody (). getSubject (); }   public String resolveToken ( HttpServletRequest req ) { String bearerToken = req . getHeader ( "Authorization" ); if ( bearerToken != null && bearerToken . startsWith ( "Bearer " )) { return bearerToken . substring ( 7 ); } return null ; }   public boolean validateToken ( String token ) { try { Jws < Claims > claims = Jwts . parser (). setSigningKey ( secretKey ). parseClaimsJws ( token );   if ( claims . getBody (). getExpiration (). before ( new Date ())) { return false ; }   return true ; } catch ( JwtException | IllegalArgumentException e ) { throw new InvalidJwtAuthenticationException ( "Expired or invalid JWT token" ); } }   }

工具类还实现了另一个功能:从HTTP请求头中获取JWT。

2.1.2 Token处理的Filter

Filter是Security处理的关键,基本上都是通过Filter来拦截请求的。首先从请求头取出JWT,然后校验JWT是否合法,如果合法则取出Authentication保存在SecurityContextHolder里。如果不合法,则做异常处理。

public class JwtTokenAuthenticationFilter extends GenericFilterBean {   private JwtTokenProvider jwtTokenProvider ;   public JwtTokenAuthenticationFilter ( JwtTokenProvider jwtTokenProvider ) { this . jwtTokenProvider = jwtTokenProvider ; }   @Override public void doFilter ( ServletRequest req , ServletResponse res , FilterChain filterChain ) throws IOException , ServletException { HttpServletRequest request = ( HttpServletRequest ) req ; HttpServletResponse response = ( HttpServletResponse ) res ;   try { String token = jwtTokenProvider . resolveToken ( request ); if ( token != null && jwtTokenProvider . validateToken ( token )) { Authentication auth = jwtTokenProvider . getAuthentication ( token );   if ( auth != null ) { SecurityContextHolder . getContext (). setAuthentication ( auth ); } } } catch ( InvalidJwtAuthenticationException e ) { response . setStatus ( HttpStatus . UNAUTHORIZED . value ()); response . getWriter (). write ( "Invalid token" ); response . getWriter (). flush (); return ; }   filterChain . doFilter ( req , res ); } }

对于异常处理,使用@ControllerAdvice是不行的,应该这个是Filter,在这里抛的异常还没有到DispatcherServlet,无法处理。所以Filter要自己做异常处理:

catch ( InvalidJwtAuthenticationException e ) { response . setStatus ( HttpStatus . UNAUTHORIZED . value ()); response . getWriter (). write ( "Invalid token" ); response . getWriter (). flush (); return ; }

最后的return不能省略,因为已经要把输出的内容给Response了,没有必要再往后传递,否则报错

java . lang . IllegalStateException : getWriter () has already been called 2.1.3 JWT属性

JWT需要配置一个密钥来加密,同时还要配置JWT令牌的有效期。

@Configuration @ConfigurationProperties ( prefix = "pkslow.jwt" ) public class JwtProperties { private String secretKey = "pkslow.key" ; private long validityInMs = 3600 _000 ; //getter and setter }

2.2 Spring Security整合

Spring Security的整个框架还是比较复杂的,简化后大概如下图所示:

它是通过一连串的Filter来进行安全管理。细节这里先不展开讲。

2.2.1 WebSecurityConfigurerAdapter配置

这个配置也可以理解为是FilterChain的配置,可以不用理解,代码很好懂它做了什么:

@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {   @Autowired JwtTokenProvider jwtTokenProvider ;   @Bean @Override public AuthenticationManager authenticationManagerBean () throws Exception { return super . authenticationManagerBean (); }   @Bean public PasswordEncoder passwordEncoder () { return NoOpPasswordEncoder . getInstance (); }   @Override protected void configure ( HttpSecurity http ) throws Exception { http . httpBasic (). disable () . csrf (). disable () . sessionManagement (). sessionCreationPolicy ( SessionCreationPolicy . STATELESS ) . and () . authorizeRequests () . antMatchers ( "/auth/login" ). permitAll () . antMatchers ( HttpMethod . GET , "/admin" ). hasRole ( "ADMIN" ) . antMatchers ( HttpMethod . GET , "/user" ). hasRole ( "USER" ) . anyRequest (). authenticated () . and () . apply ( new JwtSecurityConfigurer ( jwtTokenProvider )); } }

这里通过HttpSecurity配置了哪些请求需要什么权限才可以访问。

/auth/login用于登陆获取JWT,所以都能访问; /admin只有ADMIN用户才可以访问; /user只有USER用户才可以访问。

而之前实现的Filter则在下面配置使用:

public class JwtSecurityConfigurer extends SecurityConfigurerAdapter < DefaultSecurityFilterChain , HttpSecurity > {   private JwtTokenProvider jwtTokenProvider ;   public JwtSecurityConfigurer ( JwtTokenProvider jwtTokenProvider ) { this . jwtTokenProvider = jwtTokenProvider ; }   @Override public void configure ( HttpSecurity http ) throws Exception { JwtTokenAuthenticationFilter customFilter = new JwtTokenAuthenticationFilter ( jwtTokenProvider ); http . exceptionHandling () . authenticationEntryPoint ( new JwtAuthenticationEntryPoint ()) . and () . addFilterBefore ( customFilter , UsernamePasswordAuthenticationFilter . class ); } } 2.2.2 用户从哪来

通常在Spring Security的世界里,都是通过实现UserDetailsService来获取UserDetails的。

@Component public class CustomUserDetailsService implements UserDetailsService {   private UserRepository users ;   public CustomUserDetailsService ( UserRepository users ) { this . users = users ; }   @Override public UserDetails loadUserByUsername ( String username ) throws UsernameNotFoundException { return this . users . findByUsername ( username ) . orElseThrow (() -> new UsernameNotFoundException ( "Username: " + username + " not found" )); } }

对于 UserRepository ,可以从数据库中读取,或者其它用户管理中心。为了方便,我使用Map放了两个用户:

@Repository public class UserRepository {   private static final Map < String , User > allUsers = new HashMap <>();   @Autowired private PasswordEncoder passwordEncoder ;   @PostConstruct protected void init () { allUsers . put ( "pkslow" , new User ( "pkslow" , passwordEncoder . encode ( "123456" ), Collections . singletonList ( "ROLE_ADMIN" ))); allUsers . put ( "user" , new User ( "user" , passwordEncoder . encode ( "123456" ), Collections . singletonList ( "ROLE_USER" ))); }   public Optional < User > findByUsername ( String username ) { return Optional . ofNullable ( allUsers . get ( username )); } }

3 测试

完成代码编写后,我们来测试一下:

(1)无JWT访问,失败

curl http : //localhost:8080/admin { "timestamp" : "2021-02-06T05:45:06.385+0000" , "status" : 403 , "error" : "Forbidden" , "message" : "Access Denied" , "path" : "/admin" }   $ curl http : //localhost:8080/user { "timestamp" : "2021-02-06T05:45:16.438+0000" , "status" : 403 , "error" : "Forbidden" , "message" : "Access Denied" , "path" : "/user" }

(2)admin获取JWT,密码错误则失败,密码正确则成功

$ curl http : //localhost:8080/auth/login -X POST -d '{"username":"pkslow","password":"xxxxxx"}' -H 'Content-Type: application/json' { "timestamp" : "2021-02-06T05:47:16.254+0000" , "status" : 403 , "error" : "Forbidden" , "message" : "Access Denied" , "path" : "/auth/login" }   $ curl http : //localhost:8080/auth/login -X POST -d '{"username":"pkslow","password":"123456"}' -H 'Content-Type: application/json' eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiJwa3Nsb3ciLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sImlhdCI6MTYxMjU5MDYxNCwiZXhwIjoxNjEyNTkxMjE0fQ . d4Gi50aaOsHHqpM0d8Mh1960otnZf7rlE3x6xSfakVo

(3)admin带JWT访问/admin,成功;访问/user失败

$ curl http : //localhost:8080/admin -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwa3Nsb3ciLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sImlhdCI6MTYxMjU5MDYxNCwiZXhwIjoxNjEyNTkxMjE0fQ.d4Gi50aaOsHHqpM0d8Mh1960otnZf7rlE3x6xSfakVo' you are admin   $ curl http : //localhost:8080/user -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwa3Nsb3ciLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sImlhdCI6MTYxMjU5MDYxNCwiZXhwIjoxNjEyNTkxMjE0fQ.d4Gi50aaOsHHqpM0d8Mh1960otnZf7rlE3x6xSfakVo' { "timestamp" : "2021-02-06T05:51:23.099+0000" , "status" : 403 , "error" : "Forbidden" , "message" : "Forbidden" , "path" : "/user" }

(4)使用过期的JWT访问,失败

$ curl http : //localhost:8080/admin -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwa3Nsb3ciLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sImlhdCI6MTYxMjU5MDQ0OSwiZXhwIjoxNjEyNTkwNTA5fQ.CSaubE4iJcYATbLmbb59aNFU1jNCwDFHUV3zIakPU64' Invalid token

4 总结

代码请查看:https://github测试数据/LarryDpk/pkslow-samples

以上就是Springboot集成Spring Security实现JWT认证的步骤详解的详细内容,更多关于Springboot集成Spring Security的资料请关注其它相关文章!

查看更多关于Springboot集成Spring Security实现JWT认证的步骤详解的详细内容...

  阅读:23次