当前位置:   article > 正文

SpringBoot + SpringSecurity+JWT整合(微服务适用)

SpringBoot + SpringSecurity+JWT整合(微服务适用)

SpringSecurity基于表单的登录认证流程如下图:

大致过程是在UsernamePasswordAuthenticationFilter将username和password封装成UsernamePasswordAuthenticationToken,之后会在AuthenticationProvider(接口,需要自己实现)的authenticate方法中拿到且校验(一般是查数据库,对比账号密码),而校验成功则返回Authentication接口对象(一般情况是直接返回UsernamePasswordAuthenticationToken,该对象会默认记录在session以及SecurityContextHolder的SecurityContext中)随后走认证成功流程(一般会实现AuthenticationSuccessHandler接口),否则就抛异常走认证失败流程(一般会实现AuthenticationFailureHandler接口)。

以上是普通单体项目的认证过程,那么整合JWT或者说微服务则会有以下问题:

  1.  在哪里生成JWT?
  2. 如何解释前端传来的JWT字符串获取到用户信息?
  3. 认证成功后如何去除Session信息?(JWT是无状态的,不需要服务器端保存)JWT的详情参考

对于第一个问题,很好解决,在AuthenticationProvider认证成功后不返回UsernamePasswordAuthenticationToken,返回自定义的JwtAuthenticationToken,然后在自定义的LoginSuccessHandler返回给前端即可,代码如下:

  1. //JWT中荷载存储内容的实体类
  2. @Data
  3. public class UserDetails {
  4. private String userId;
  5. private String username;
  6. private String portrait;
  7. }
  8. //用来生成和解释Token字符串,这里采用了单例模式,相当于JwtUtils
  9. public class UserTokenManager {
  10. private final String SECRET = "happy-king";
  11. private static volatile UserTokenManager userTokenManager;
  12. private UserTokenManager() {
  13. }
  14. public static UserTokenManager getInstance() {
  15. if (userTokenManager == null) {
  16. synchronized (UserTokenManager.class) {
  17. if (userTokenManager == null) {
  18. userTokenManager = new UserTokenManager();
  19. }
  20. }
  21. }
  22. return userTokenManager;
  23. }
  24. @SuppressWarnings("unchecked")
  25. public String generateToken(UserDetails userDetails, long timeout) {
  26. long currentTimeMillis = System.currentTimeMillis();
  27. return JWT.create()
  28. .withJWTId(UUID.randomUUID().toString())
  29. .withIssuedAt(new Date(currentTimeMillis))
  30. .withClaim("user_id", userDetails.getUserId())
  31. .withClaim("username", userDetails.getUsername())
  32. .withClaim("portrait", userDetails.getPortrait())
  33. .withExpiresAt(new Date(currentTimeMillis + timeout))
  34. .sign(Algorithm.HMAC256(SECRET));
  35. }
  36. public String generateToken(UserDetails userDetails) {
  37. return generateToken(userDetails, 1000 * 24 * 60 * 60L);
  38. }
  39. public UserDetails parseToken(String token) {
  40. try {
  41. DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
  42. Date issuedAt = decodedJWT.getIssuedAt();
  43. if (issuedAt.before(new Date())) {
  44. String userId = decodedJWT.getClaim("user_id").asString();
  45. String username = decodedJWT.getClaim("username").asString();
  46. String portrait = decodedJWT.getClaim("portrait").asString();
  47. UserDetails userDetails = new UserDetails();
  48. userDetails.setUsername(username);
  49. userDetails.setUserId(userId);
  50. userDetails.setPortrait(portrait);
  51. return userDetails;
  52. } else {
  53. return null;
  54. }
  55. } catch (Exception e) {
  56. return null;
  57. }
  58. }
  59. }
  60. //自定义的Authentication类
  61. public class JwtAuthenticationToken extends AbstractAuthenticationToken {
  62. private final UserDetails userDetails;
  63. private final UserTokenManager userTokenManager = UserTokenManager.getInstance();
  64. public JwtAuthenticationToken(UserDetails userDetails,
  65. Collection<? extends GrantedAuthority> authorities) {
  66. super(authorities);
  67. this.userDetails = userDetails;
  68. }
  69. @Override
  70. public Object getCredentials() {
  71. return null;
  72. }
  73. @Override
  74. public Object getPrincipal() {
  75. return userDetails;
  76. }
  77. public String generateJWT(){
  78. return userTokenManager.generateToken(userDetails);
  79. }
  80. }
  81. //登录认证主要逻辑类
  82. public class AuthenticationProvider implements org.springframework.security.authentication.AuthenticationProvider {
  83. @Override
  84. public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  85. UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
  86. //获取账号密码
  87. String username = (String) token.getPrincipal();
  88. String password = (String) token.getCredentials();
  89. //省略了查数据库以及对比流程
  90. UserDetails userDetails = new UserDetails();
  91. userDetails.setUsername(username);
  92. userDetails.setUserId(UUID.randomUUID().toString());
  93. //在微服务中,角色和权限信息要保存在Redis中
  94. SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_xx");
  95. JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(userDetails, Collections.singleton(simpleGrantedAuthority));
  96. //必须设置为认证成功
  97. jwtAuthenticationToken.setAuthenticated(true);
  98. return jwtAuthenticationToken;
  99. }
  100. @Override
  101. public boolean supports(Class<?> authentication) {
  102. return authentication == JwtAuthenticationToken.class || authentication == UsernamePasswordAuthenticationToken.class;
  103. }
  104. }
  105. //然后登录成功直接写token响应到前端即可
  106. public class LoginSuccessHandler implements AuthenticationSuccessHandler {
  107. private final ObjectMapper objectMapper;
  108. public LoginSuccessHandler(ObjectMapper objectMapper){
  109. this.objectMapper = objectMapper;
  110. }
  111. @Override
  112. public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
  113. JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;
  114. response.setContentType("application/json;charset=utf-8");
  115. HashMap<String, Object> map = new HashMap<>();
  116. map.put("token", jwtAuthenticationToken.generateJWT());
  117. String result = objectMapper.writeValueAsString(map);
  118. response.getWriter().write(result);
  119. }
  120. }

需要注意必须执行以下代码:

JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(userDetails, Collections.singleton(simpleGrantedAuthority));
jwtAuthenticationToken.setAuthenticated(true);

因为除了认证接口外,其他所有需要授权()的接口都会被AbstractSecurityInterceptor拦截,里面有一个authenticateIfRequired方法,如果没有调用

jwtAuthenticationToken.setAuthenticated(true);则会重新进行用户认证。源码如下:

  1. private Authentication authenticateIfRequired() {
  2. Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
  3. if (authentication.isAuthenticated() && !this.alwaysReauthenticate) {
  4. if (this.logger.isTraceEnabled()) {
  5. this.logger.trace(LogMessage.format("Did not re-authenticate %s before authorizing", authentication));
  6. }
  7. return authentication;
  8. }
  9. authentication = this.authenticationManager.authenticate(authentication);
  10. // Don't authenticated.setAuthentication(true) because each provider does that
  11. if (this.logger.isDebugEnabled()) {
  12. this.logger.debug(LogMessage.format("Re-authenticated %s before authorizing", authentication));
  13. }
  14. SecurityContextHolder.getContext().setAuthentication(authentication);
  15. return authentication;
  16. }

第二个问题则需要用到SpringSecurity中的拦截器,我们需要自定义一个JwtFilter,并且将其加入到UsernamePasswordAuthenticationFilter之后,代码如下:

  1. public final class JwtFilter extends GenericFilterBean {
  2. private final static String BEARER = "bearer";
  3. private final UserTokenManager userTokenManager = UserTokenManager.getInstance();
  4. @Override
  5. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
  6. throws IOException, ServletException {
  7. doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
  8. }
  9. /**
  10. *以下抛出的异常你可以改为写出JSON响应
  11. **/
  12. private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
  13. Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
  14. if (authentication != null) {
  15. chain.doFilter(request, response);
  16. return;
  17. }
  18. String bearer = request.getHeader(BEARER);
  19. if (bearer == null || bearer.isEmpty()){
  20. bearer = request.getParameter(BEARER);
  21. }
  22. if (bearer == null || bearer.isEmpty()){
  23. //没有登录
  24. throw new AuthenticationCredentialsNotFoundException("没有用户凭证");
  25. }else{
  26. UserDetails userDetails = userTokenManager.parseToken(bearer);
  27. if (userDetails == null){
  28. throw new CredentialsExpiredException("用户凭证失效");
  29. }
  30. //微服务项目角色权限信息需要重Redis中获取,这里只是模拟
  31. SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_xx");
  32. JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(userDetails, Collections.singleton(simpleGrantedAuthority));
  33. jwtAuthenticationToken.setAuthenticated(true);
  34. //注意要将解释出来的JwtAuthenticationToken信息保存到SecurityContext中
  35. SecurityContextHolder
  36. .getContext()
  37. .setAuthentication(jwtAuthenticationToken);
  38. }
  39. chain.doFilter(request, response);
  40. }
  41. }

至此单体项目整合SpringSecurit 和JWT登录已经完成,用户登录成功后,SecurityContextPersistenceFilter中的SecurityContextRepository(默认实现是HttpSessionSecurityContextRepository)会将JwtAuthenticationToken写到session中。但是因为JWT用于分布式系统或者微服务的,所以我们不能用Session来管理,只需要修改默认的HttpSessionSecurityContextRepository为NullSecurityContextRepository并关闭session管理即可。

代码如下:

  1. @Override
  2. protected void configure(HttpSecurity http) throws Exception {
  3. //移除默认登录页面
  4. http.removeConfigurer(DefaultLoginPageConfigurer.class);
  5. //关闭csrf
  6. http.csrf().disable()
  7. //关闭请求缓存
  8. .requestCache().disable()
  9. //关闭session管理
  10. .sessionManagement().disable()
  11. //修改SecurityContextRepository
  12. .securityContext().securityContextRepository(new NullSecurityContextRepository()).and()
  13. .addFilterAfter(new JwtFilter(), UsernamePasswordAuthenticationFilter.class)
  14. .authorizeRequests().anyRequest().permitAll().and()
  15. .formLogin()
  16. .loginPage("/login")
  17. .failureHandler(new LoginFailureHandler(objectMapper))
  18. .successHandler(new LoginSuccessHandler(objectMapper))
  19. .permitAll()
  20. .and()
  21. .httpBasic();
  22. }

 至此,整合工作已经完毕,下面提供了代码下载地址,以上流程和WebSecurityConfigurerAdapter的实现配置用于用户微服务,然后在不同的服务下定义不同的WebSecurityConfigurerAdapter然后设置formLogin().disable()即可

示例代码下载

 

 

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Li_阴宅/article/detail/885796
推荐阅读
相关标签
  

闽ICP备14008679号