IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> SpringBoot + SpringSecurity+JWT整合(微服务适用) -> 正文阅读

[Java知识库]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()即可

示例代码下载

?

?

?

?

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-09-27 13:57:03  更:2021-09-27 13:58:58 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 19:12:48-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码