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

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

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-17 05:01:29 +0800 CST
Grid布局的简洁性和高效性
2024-11-18 03:48:02 +0800 CST
纯CSS实现3D云动画效果
2024-11-18 18:48:05 +0800 CST
快速提升Vue3开发者的效率和界面
2025-05-11 23:37:03 +0800 CST
php机器学习神经网络库
2024-11-19 09:03:47 +0800 CST
开发外贸客户的推荐网站
2024-11-17 04:44:05 +0800 CST
Go语言中实现RSA加密与解密
2024-11-18 01:49:30 +0800 CST
html折叠登陆表单
2024-11-18 19:51:14 +0800 CST
25个实用的JavaScript单行代码片段
2024-11-18 04:59:49 +0800 CST
微信内弹出提示外部浏览器打开
2024-11-18 19:26:44 +0800 CST
支付轮询打赏系统介绍
2024-11-18 16:40:31 +0800 CST
16.6k+ 开源精准 IP 地址库
2024-11-17 23:14:40 +0800 CST
如何在Vue3中定义一个组件?
2024-11-17 04:15:09 +0800 CST
Vue3中如何扩展VNode?
2024-11-17 19:33:18 +0800 CST
Vue3中如何处理WebSocket通信?
2024-11-19 09:50:58 +0800 CST
Vue3中的JSX有什么不同?
2024-11-18 16:18:49 +0800 CST
Golang 中应该知道的 defer 知识
2024-11-18 13:18:56 +0800 CST
企业官网案例-芊诺网络科技官网
2024-11-18 11:30:20 +0800 CST
程序员茄子在线接单