最近在重构公司的后端鉴权模块,发现很多新人对 JWT(JSON Web Token)的理解只停留在“调库生成一段字符串”的阶段。一旦上线到生产环境,各种业务客诉就来了:用户填了半天表单,点提交时 Token 过期直接跳回登录页;或者安全部门来扫描,问你 Token 被截获了怎么强制让用户下线?

纯“无状态”的 JWT 在真实业务中其实是个伪命题。今天借着 GitHub 上一个非常经典的优质开源项目——spring-boot-refresh-token-jwt by bezkoder,来聊聊我们在生产环境中到底该怎么处理 JWT 的这几个老生常谈的痛点。

1. jwt token过期解决方法:双 Token 与数据库状态化

痛点: Token 有效期设长了不安全,设短了用户体验极差。

很多人第一反应是“自动续期”,每次请求都在 Header 里塞一个新的 Token 返回给前端。这其实非常浪费带宽和后端计算资源。
主流且优雅的做法是引入 Access Token (AT) + Refresh Token (RT) 的双 Token 机制。

我们来看看 bezkoder 这个开源项目是怎么巧妙实现的。它并没有让 Refresh Token 也是个无状态的 JWT,而是把 Refresh Token 存在了数据库里

核心逻辑拆解:

  1. 登录时:后端生成一个寿命短的 Access Token(比如 15 分钟),同时生成一个随机 UUID 作为 Refresh Token(寿命设为 7 天),并将这个 RT 保存到数据库表 refresh_tokens 中,关联当前 UserID。
  2. 正常请求:前端带着 AT 请求接口,没过期就正常放行。
  3. AT 过期:前端拿到 401 状态码,立刻静默拿着之前的 RT 去请求 /api/auth/refreshtoken 接口。

看一眼该项目库里的核心刷新逻辑:

@PostMapping("/refreshtoken")
public ResponseEntity<?> refreshtoken(@Valid @RequestBody TokenRefreshRequest request) {
    String requestRefreshToken = request.getRefreshToken();

    // 从数据库中查找这个 Refresh Token
    return refreshTokenService.findByToken(requestRefreshToken)
        .map(refreshTokenService::verifyExpiration) // 校验数据库里的 RT 是否过期
        .map(RefreshToken::getUser)
        .map(user -> {
            // 生成新的 Access Token
            String token = jwtUtils.generateTokenFromUsername(user.getUsername());
            return ResponseEntity.ok(new TokenRefreshResponse(token, requestRefreshToken));
        })
        .orElseThrow(() -> new TokenRefreshException(requestRefreshToken, "Refresh token is not in database!"));
}

为什么要把 RT 存数据库? 因为这不仅解决了无感刷新,还顺带解决了一个极其棘手的安全问题(详见第 4 点)。

2. jwt token为空怎么处理?

痛点: 难道要在每一个 Controller 接口里都写一句 if(token == null) 吗?

这其实是网关或拦截器该干的事。在基于 Spring Security 的项目中(如上文的开源项目),通常会自定义一个 AuthTokenFilter
对于那些非公开的接口,如果 Token 为空,不应该把异常抛到业务层,而应该在 Filter 层面直接打回。

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
    try {
        String jwt = parseJwt(request);
        // token 为空或校验不通过,根本走不到业务逻辑
        if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
            // 组装 Authentication 放入 SecurityContext
            // ...
        }
    } catch (Exception e) {
        logger.error("Cannot set user authentication: {}", e.getMessage());
    }
    filterChain.doFilter(request, response);
}

如果是普通的 Spring Web 项目,写一个 HandlerInterceptor,在 preHandle 里判断请求头 Authorization。为空直接 response.setStatus(401),返回统一的 JSON 错误码即可。

3. jwt token获取用户信息:拒绝重复解析

痛点: 很多开发喜欢在 Service 层需要用到 userId 的时候,再去解析一遍 Token,或者层层传参。这既耗费 CPU,又让代码耦合严重。

正解是配合 ThreadLocal 实现上下文透传。
一旦我们在前面的拦截器(Filter/Interceptor)中校验 Token 合法,就顺手把 Payload 里的 userId 解析出来,塞进当前线程的上下文中。

// 1. 定义一个简单的 Context
public class UserContext {
    private static final ThreadLocal<String> USER_ID = new ThreadLocal<>();
    public static void set(String userId) { USER_ID.set(userId); }
    public static String get() { return USER_ID.get(); }
    public static void remove() { USER_ID.remove(); }
}

// 2. 在 Controller/Service 中直接拿,再也不用碰 Token
public void createOrder() {
    String currentUserId = UserContext.get();
    // 业务逻辑...
}

注意:由于 Tomcat 的线程池复用机制,切记在请求结束(afterCompletion)时调用 UserContext.remove(),否则会导致严重的内存泄漏和用户串号问题。

4. jwt token被盗怎么解决?

痛点: 纯无状态的 JWT 最大的死穴——一旦颁发,后端无法主动废弃。如果黑客通过 XSS 窃取了用户的 Token,怎么强制让他下线?

这时候,前文提到的 bezkoder 开源项目中**“数据库存储 Refresh Token”**的威力就体现出来了。

  • 针对 Access Token: 既然我们把 AT 的寿命压得很短(15分钟),黑客拿到了也只能嚣张一小会儿。
  • 针对 Refresh Token: 这是一个长效 Token。如果用户发现账号异地登录,或者管理员需要封禁某个用户,直接在数据库的 refresh_tokens 表中,DELETE 掉该用户关联的记录即可

黑客手里的 AT 过期后,必然会带着偷来的 RT 去换新 Token。此时后端去数据库一查:RT 不存在,直接抛出 TokenRefreshException,强制黑客重新走账密登录。

进阶防盗手段:绑定指纹
在生成 Token 时,提取用户当前请求的 User-AgentIP 地址 取 MD5,一起塞进 JWT 的 Payload 里。校验 Token 时,如果发现当前请求的 IP 变了,直接判定 Token 无效。这招防 XSS 盗用非常有效。

  • 重构后的 SpringBoot 双 Token 核心源码(全中文注释版)。
  • Vue3 配合无感刷新的完整前端登录页与 Axios 拦截器配置文件。
  • 配套的 MySQL 基础建表脚本与 Postman 接口调试集合。

👇👇 全栈双 Token 落地包获取方式 👇👇

链接:https://pan.quark.cn/s/1f0215d12525
提取码:gd7c

其实,把 JWT 用在传统的 Web Session 鉴权场景里,为了弥补其无状态带来的缺陷,我们通常都会把它“重新状态化”(引入数据库/Redis)。

如果你目前正在做这方面的需求,强烈建议 Clone 一下 https://github.com/bezkoder/spring-boot-refresh-token-jwt 这个仓库跑一遍。看看别人是怎么用 Spring Data JPA 优雅地管理 Refresh Token 生命周期的,这比你自己从零开始造轮子要稳健得多。

大家在公司项目里,是怎么处理多设备登录被踢下线这类 JWT 场景的?欢迎在评论区探讨。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐