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

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

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的无感刷新,使用户体验更加流畅。

推荐文章

HTML + CSS 实现微信钱包界面
2024-11-18 14:59:25 +0800 CST
页面不存在404
2024-11-19 02:13:01 +0800 CST
一些实用的前端开发工具网站
2024-11-18 14:30:55 +0800 CST
Vue 3 中的 Fragments 是什么?
2024-11-17 17:05:46 +0800 CST
markdown语法
2024-11-18 18:38:43 +0800 CST
PHP 的生成器,用过的都说好!
2024-11-18 04:43:02 +0800 CST
Flet 构建跨平台应用的 Python 框架
2025-03-21 08:40:53 +0800 CST
Rust 并发执行异步操作
2024-11-19 08:16:42 +0800 CST
Vue3中的v-slot指令有什么改变?
2024-11-18 07:32:50 +0800 CST
38个实用的JavaScript技巧
2024-11-19 07:42:44 +0800 CST
php 统一接受回调的方案
2024-11-19 03:21:07 +0800 CST
nuxt.js服务端渲染框架
2024-11-17 18:20:42 +0800 CST
Nginx 防止IP伪造,绕过IP限制
2025-01-15 09:44:42 +0800 CST
Vue3的虚拟DOM是如何提高性能的?
2024-11-18 22:12:20 +0800 CST
XSS攻击是什么?
2024-11-19 02:10:07 +0800 CST
Elasticsearch 条件查询
2024-11-19 06:50:24 +0800 CST
goctl 技术系列 - Go 模板入门
2024-11-19 04:12:13 +0800 CST
Golang实现的交互Shell
2024-11-19 04:05:20 +0800 CST
IP地址获取函数
2024-11-19 00:03:29 +0800 CST
一文详解回调地狱
2024-11-19 05:05:31 +0800 CST
程序员茄子在线接单