编程 SpringBoot 实现一人一号,无感刷新Jwt

2024-11-19 03:12:05 +0800 CST views 540

SpringBoot实战:SpringBoot 实现一人一号,无感刷新Jwt

引言

在现代应用的安全架构中,用户认证与授权机制占据着核心地位。特别是在多设备登录和高频请求的环境中,确保每位用户仅能通过一个账号登录,并有效管理Token的刷新策略,成为了后端开发中的重要挑战。通过整合Spring Boot 3、Spring Security 6、JWT(JSON Web Tokens)以及Redis等先进技术,可以构建出一个高效、安全的用户认证体系。本文将详细阐述如何实现“一人一号”的登录限制以及Token的无感刷新功能,以提升系统的安全性和用户体验。

一、一人一号认证

JwtTokenFilter拦截器中,核心任务是解析请求中的JWT Token,并与Redis中存储的Token进行比对,确保用户的Token有效且未被篡改。这种机制确保了当用户的Token变更后,任何旧的或失效的Token都将被拒绝访问,从而增强系统的安全性。

1.1 JwtTokenFilter拦截器代码

@Component
@RequiredArgsConstructor
public class JwtTokenFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final RedisUtil redisUtil;
    private final SystemConfiguration systemConfiguration;
    private final ServerProperties properties;

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
        String token = jwtUtil.removeTokenPrefix(request);
        String uri = request.getRequestURI();
        String contextPath = properties.getServlet().getContextPath();
        if (StringUtils.hasText(contextPath)) {
            uri = uri.substring(contextPath.length());
        }

        if (!SecurityUtil.isWhitelisted(uri, systemConfiguration.getSecurityWhitelistPaths()) && StringUtils.hasText(token)) {
            Authentication auth = jwtUtil.getAuthentication(token);
            if (auth == null) {
                if (jwtUtil.isJwtExpired(token)) {
                    ResponseUtil.writeIResultCodeMsg(response, HttpStatus.UNAUTHORIZED, ResultCode.AUTH_TOKEN_EXPIRED);
                } else {
                    ResponseUtil.writeIResultCodeMsg(response, HttpStatus.UNAUTHORIZED, ResultCode.AUTH_TOKEN_INVALID);
                }
                return;
            }

            Long userId = SecurityUtil.getUserId(auth);
            if (userId == null) {
                ResponseUtil.writeIResultCodeMsg(response, HttpStatus.UNAUTHORIZED, ResultCode.AUTH_TOKEN_INVALID);
                return;
            }

            LoginResult loginResult = redisUtil.getCacheObject(RedisKeyConstants.USER_TOKEN_CACHE_PREFIX + userId);
            if (loginResult == null) {
                ResponseUtil.writeIResultCodeMsg(response, HttpStatus.UNAUTHORIZED, ResultCode.AUTH_KICK_OUT);
                return;
            }
            if (!token.equals(loginResult.getAccessToken())) {
                ResponseUtil.writeIResultCodeMsg(response, HttpStatus.FORBIDDEN, ResultCode.AUTH_USER_ELSEWHERE_LOGIN);
                return;
            }
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        filterChain.doFilter(request, response);
    }
}

1.2 代码解析

  • OncePerRequestFilter:确保在每个请求中只执行一次过滤操作。
  • JwtUtil:用于解析和验证JWT Token。
  • RedisUtil:用于与Redis进行交互,存储和验证用户的Token。
  • 核心流程是将请求中的Token与Redis中存储的Token进行比对,不一致时则拒绝访问,确保“一人一号”的原则。

1.3 登录JWT流程

每次用户登录后,系统会将新的Token存入Redis,并覆盖掉旧的Token。当用户使用旧Token尝试访问时,拦截器会拒绝该请求。

public LoginResult getLoginResult(Authentication authenticate) {
    if (authenticate == null || authenticate.getPrincipal() == null) {
        return null;
    }
    SysUserDetails principal = (SysUserDetails) authenticate.getPrincipal();
    Duration accessTokenExpirationTime = jwtConfiguration.getAccessTokenExpirationTime();
    Duration refreshTokenExpirationTime = jwtConfiguration.getRefreshTokenExpirationTime();
    String accessToken = generateAccessToken(authenticate);
    String refreshToken = generateRefreshToken(authenticate);

    LoginResult result = LoginResult.builder()
        .accessToken(accessToken)
        .refreshToken(refreshToken)
        .expires(Date.from(Instant.now().plus(accessTokenExpirationTime)).getTime())
        .build();

    redisUtil.setCacheObject(RedisKeyConstants.USER_TOKEN_CACHE_PREFIX + principal.getUserId(), result, refreshTokenExpirationTime.toMillis(), TimeUnit.MILLISECONDS);
    redisUtil.setCacheObject(RedisKeyConstants.USER_PERMISSIONS_CACHE_PREFIX + principal.getUserId(), principal.getPermissions(), refreshTokenExpirationTime.toMillis(), TimeUnit.MILLISECONDS);

    return result;
}

二、无感刷新Token

无感刷新Token是通过解析和验证用户的AccessToken与RefreshToken,确保在合法情况下才能刷新Token。以下是无感刷新Token的实现。

2.1 Token刷新流程

@Override
public LoginResult refreshToken(RefreshTokenForm refreshTokenForm) {
    if (!jwtUtil.isJwtExpired(refreshTokenForm.getAccessToken())) {
        throw new ServiceException(ResultCode.AUTH_MALICIOUS_TOKEN_REFRESH);
    }

    Long userId = jwtUtil.getRefreshTokenUserId(refreshTokenForm.getRefreshToken());
    if (userId == null) {
        if (jwtUtil.isJwtExpired(refreshTokenForm.getRefreshToken())) {
            throw new ServiceException(ResultCode.AUTH_TOKEN_EXPIRED);
        } else {
            throw new ServiceException(ResultCode.AUTH_MALICIOUS_TOKEN_REFRESH);
        }
    }

    LoginResult loginResult = redisUtil.getCacheObject(RedisKeyConstants.USER_TOKEN_CACHE_PREFIX + userId);
    if (loginResult == null) {
        throw new ServiceException(ResultCode.AUTH_TOKEN_EXPIRED);
    }
    if (!refreshTokenForm.getAccessToken().equals(loginResult.getAccessToken()) || !refreshTokenForm.getRefreshToken().equals(loginResult.getRefreshToken())) {
        throw new ServiceException(ResultCode.AUTH_MALICIOUS_TOKEN_REFRESH);
    }

    return jwtUtil.refreshToken(refreshTokenForm);
}

2.2 代码解析

  • AccessToken过期检测:如果AccessToken未过期,则视为恶意刷新,抛出异常。
  • RefreshToken验证:通过解析RefreshToken获取用户ID,并与Redis中存储的Token进行比对,确保一致性。
  • 返回新Token:验证通过后,系统生成新的AccessToken与RefreshToken并返回,保持会话连续性。

2.3 关键点解析

  • 验证AccessToken是否过期:通过jwtUtil.isJwtExpired方法检测用户的AccessToken是否过期,未过期的刷新请求将视为恶意行为。
  • 检验Redis中的Token信息:确保用户提交的Token与Redis中存储的完全一致,防止非法刷新。

通过该机制,系统确保了用户认证过程的安全性,同时实现了Token的无感刷新,使用户体验更加流畅。

推荐文章

实现微信回调多域名的方法
2024-11-18 09:45:18 +0800 CST
Roop是一款免费开源的AI换脸工具
2024-11-19 08:31:01 +0800 CST
前端如何一次性渲染十万条数据?
2024-11-19 05:08:27 +0800 CST
前端代码规范 - 图片相关
2024-11-19 08:34:48 +0800 CST
软件定制开发流程
2024-11-19 05:52:28 +0800 CST
#免密码登录服务器
2024-11-19 04:29:52 +0800 CST
Vue3中如何处理组件间的动画?
2024-11-17 04:54:49 +0800 CST
使用 Nginx 获取客户端真实 IP
2024-11-18 14:51:58 +0800 CST
JavaScript设计模式:组合模式
2024-11-18 11:14:46 +0800 CST
Nginx 如何防止 DDoS 攻击
2024-11-18 21:51:48 +0800 CST
Vue3中的事件处理方式有何变化?
2024-11-17 17:10:29 +0800 CST
程序员茄子在线接单