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返回给前端即可,代码如下:

//JWT中荷载存储内容的实体类
@Data
public class UserDetails {
    private String userId;
    private String username;
    private String portrait;
}
//用来生成和解释Token字符串,这里采用了单例模式,相当于JwtUtils
public class UserTokenManager {
    private final String SECRET = "happy-king";
    private static volatile UserTokenManager userTokenManager;

    private UserTokenManager() {
    }

    public static UserTokenManager getInstance() {
        if (userTokenManager == null) {
            synchronized (UserTokenManager.class) {
                if (userTokenManager == null) {
                    userTokenManager = new UserTokenManager();
                }
            }
        }
        return userTokenManager;
    }

    @SuppressWarnings("unchecked")
    public String generateToken(UserDetails userDetails, long timeout) {
        long currentTimeMillis = System.currentTimeMillis();
        return JWT.create()
                .withJWTId(UUID.randomUUID().toString())
                .withIssuedAt(new Date(currentTimeMillis))
                .withClaim("user_id", userDetails.getUserId())
                .withClaim("username", userDetails.getUsername())
                .withClaim("portrait", userDetails.getPortrait())
                .withExpiresAt(new Date(currentTimeMillis + timeout))
                .sign(Algorithm.HMAC256(SECRET));
    }

    public String generateToken(UserDetails userDetails) {
        return generateToken(userDetails, 1000 * 24 * 60 * 60L);
    }

    public UserDetails parseToken(String token) {
        try {
            DecodedJWT decodedJWT =         JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
            Date issuedAt = decodedJWT.getIssuedAt();
            if (issuedAt.before(new Date())) {
                String userId = decodedJWT.getClaim("user_id").asString();
                String username = decodedJWT.getClaim("username").asString();
                String portrait = decodedJWT.getClaim("portrait").asString();
                UserDetails userDetails = new UserDetails();
                userDetails.setUsername(username);
                userDetails.setUserId(userId);
                userDetails.setPortrait(portrait);
                return userDetails;
            } else {
                return null;
            }
        } catch (Exception e) {
            return null;
        }
    }
}
//自定义的Authentication类
public class JwtAuthenticationToken  extends AbstractAuthenticationToken {
    private final UserDetails userDetails;
    private final UserTokenManager userTokenManager = UserTokenManager.getInstance();

    public JwtAuthenticationToken(UserDetails userDetails,
            Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.userDetails = userDetails;
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return userDetails;
    }

    public String generateJWT(){
        return userTokenManager.generateToken(userDetails);
    }
}
//登录认证主要逻辑类
public class AuthenticationProvider implements org.springframework.security.authentication.AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
        //获取账号密码
        String username = (String) token.getPrincipal();
        String password = (String) token.getCredentials();
        //省略了查数据库以及对比流程

        UserDetails userDetails = new UserDetails();
        userDetails.setUsername(username);
        userDetails.setUserId(UUID.randomUUID().toString());
        //在微服务中,角色和权限信息要保存在Redis中
        SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_xx");
         JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(userDetails, Collections.singleton(simpleGrantedAuthority));
        //必须设置为认证成功
        jwtAuthenticationToken.setAuthenticated(true);
        return jwtAuthenticationToken;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication == JwtAuthenticationToken.class || authentication == UsernamePasswordAuthenticationToken.class;
    }
}
//然后登录成功直接写token响应到前端即可
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    private final ObjectMapper objectMapper;
    public LoginSuccessHandler(ObjectMapper objectMapper){
        this.objectMapper = objectMapper;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;
        response.setContentType("application/json;charset=utf-8");
        HashMap<String, Object> map = new HashMap<>();
        map.put("token", jwtAuthenticationToken.generateJWT());
        String result = objectMapper.writeValueAsString(map);
        response.getWriter().write(result);
    }
}

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

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

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

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

private Authentication authenticateIfRequired() {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if (authentication.isAuthenticated() && !this.alwaysReauthenticate) {
			if (this.logger.isTraceEnabled()) {
				this.logger.trace(LogMessage.format("Did not re-authenticate %s before authorizing", authentication));
			}
			return authentication;
		}
		authentication = this.authenticationManager.authenticate(authentication);
		// Don't authenticated.setAuthentication(true) because each provider does that
		if (this.logger.isDebugEnabled()) {
			this.logger.debug(LogMessage.format("Re-authenticated %s before authorizing", authentication));
		}
		SecurityContextHolder.getContext().setAuthentication(authentication);
		return authentication;
	}

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

public final class JwtFilter  extends GenericFilterBean {
    private final static String BEARER = "bearer";

    private final UserTokenManager userTokenManager = UserTokenManager.getInstance();

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
    }

    /**
*以下抛出的异常你可以改为写出JSON响应
**/
    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null) {
            chain.doFilter(request, response);
            return;
        }
        String bearer = request.getHeader(BEARER);
        if (bearer == null || bearer.isEmpty()){
            bearer = request.getParameter(BEARER);
        }
        if (bearer == null || bearer.isEmpty()){
            //没有登录
            throw new AuthenticationCredentialsNotFoundException("没有用户凭证");
        }else{
            UserDetails userDetails = userTokenManager.parseToken(bearer);
            if (userDetails == null){
                throw new CredentialsExpiredException("用户凭证失效");
            }
            //微服务项目角色权限信息需要重Redis中获取,这里只是模拟
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_xx");
            JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(userDetails, Collections.singleton(simpleGrantedAuthority));
            jwtAuthenticationToken.setAuthenticated(true);
            //注意要将解释出来的JwtAuthenticationToken信息保存到SecurityContext中
            SecurityContextHolder
                    .getContext()
                    .setAuthentication(jwtAuthenticationToken);
        }
        chain.doFilter(request, response);
    }
}

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

代码如下:

 @Override
    protected void configure(HttpSecurity http) throws Exception {
            //移除默认登录页面
        http.removeConfigurer(DefaultLoginPageConfigurer.class);
        //关闭csrf
        http.csrf().disable()
//关闭请求缓存
                .requestCache().disable()
//关闭session管理
                .sessionManagement().disable()
//修改SecurityContextRepository
.securityContext().securityContextRepository(new NullSecurityContextRepository()).and()
                .addFilterAfter(new JwtFilter(), UsernamePasswordAuthenticationFilter.class)
                
                .authorizeRequests().anyRequest().permitAll().and()
                .formLogin()
                .loginPage("/login")
                .failureHandler(new LoginFailureHandler(objectMapper))
                .successHandler(new LoginSuccessHandler(objectMapper))
                .permitAll()
                .and()
                .httpBasic();
    }

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

示例代码下载

 

 

 

 

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值