SpringBoot 无感刷新 Token 全解析
2025-06-25 13:08 阅读(40)

背景问题:为什么需要无感刷新?

想象这样一个场景:


“我正在后台管理系统中录入数据,页面突然跳转回登录界面,之前填写的内容全没了!”


这是典型的 Token 到期导致会话失效 的问题,尤其在使用 Redis 等缓存中间件存储 Token 时尤为常见。

问题根源

后端通常通过 JWT 来实现无状态身份验证,但 JWT 的缺陷也很明显:过期即失效,无法修改或撤销。如果不设计 Token 刷新机制,用户体验将大打折扣。

核心策略:Token 无感续签方案概述

方案一:后端自动续期(推荐)

在每次用户请求时,后端检查当前 Token 的有效时间:


若临近过期(如小于5分钟) ,则动态生成一个新 Token,加入响应头中返回;

前端拦截响应头,若发现新的 Token,与本地不一致则自动更新本地 Token。


方案二:前端主动续签(补充方案)


前端维护一对 Token:access_token(短期)+ refresh_token(长期);

每隔一段时间,前端使用 refresh_token 去调用刷新接口,获取新的 access_token。


后端实现细节

依赖配置(pom.xml)

<dependencies>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.5.1</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.33</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
</dependencies>


JWT 工具类 JwtUtil.java

代码路径:/src/main/java/com/demo/auth/utils/JwtUtil.java

package com.demo.auth.utils;


import io.jsonwebtoken.*;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;


public class JwtUtil {
    public static final long JWT_TTL = 1000L * 60 * 60 * 24; // 24小时
    public static final String JWT_KEY = "qx";


    public static String createJWT(String subject) {
        return getJwtBuilder(subject, null, UUID.randomUUID().toString().replace("-", "")).compact();
    }


    public static String createJWT(String subject, Long ttlMillis) {
        return getJwtBuilder(subject, ttlMillis, UUID.randomUUID().toString()).compact();
    }


    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        long nowMillis = System.currentTimeMillis();
        long expMillis = (ttlMillis != null ? nowMillis + ttlMillis : nowMillis + JWT_TTL);
        SecretKey secretKey = generalKey();
        return Jwts.builder()
                .setId(uuid)
                .setSubject(subject)
                .setIssuer("icoderoad")
                .setIssuedAt(new Date(nowMillis))
                .setExpiration(new Date(expMillis))
                .signWith(SignatureAlgorithm.HS256, secretKey);
    }


    public static Claims parseJWT(String jwt) throws Exception {
        return Jwts.parser()
                .setSigningKey(generalKey())
                .parseClaimsJws(jwt)
                .getBody();
    }


    public static SecretKey generalKey() {
        byte[] key = Base64.getDecoder().decode(JWT_KEY);
        return new SecretKeySpec(key, 0, key.length, "AES");
    }


    public static Date getExpiration(String jwt) {
        try {
            return parseJWT(jwt).getExpiration();
        } catch (Exception e) {
            throw new RuntimeException("Token 解析失败", e);
        }
    }
}


Token 拦截与续签逻辑

拦截器路径:/src/main/java/com/demo/auth/interceptor/AuthInterceptor.java

public class AuthInterceptor implements HandlerInterceptor {


    private static final long REFRESH_THRESHOLD = 1000L * 60 * 5; // 剩余5分钟内刷新


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {


        String token = request.getHeader("Authorization");
        if (StringUtils.isEmpty(token)) {
            throw new RuntimeException("未登录");
        }


        Claims claims = JwtUtil.parseJWT(token);
        long now = System.currentTimeMillis();
        long exp = claims.getExpiration().getTime();


        if (exp - now < REFRESH_THRESHOLD) {
            String newToken = JwtUtil.createJWT(claims.getSubject());
            response.setHeader("X-Token-Refresh", newToken);
        }


        return true;
    }
}


前端处理逻辑(以 Vue + Axios 为例)

前端拦截代码:

axios.interceptors.response.use(response => {
    const newToken = response.headers['x-token-refresh'];
    if (newToken && newToken !== localStorage.getItem('access_token')) {
        localStorage.setItem('access_token', newToken);
    }
    return response;
}, error => {
    // 处理401
    if (error.response.status === 401) {
        // 可以保存草稿后跳转登录
    }
    return Promise.reject(error);
});


关于 AccessToken 和 RefreshToken 的机制说明

类型用途特点
AccessToken 携带用户身份,频繁使用安全风险高,需短时过期
RefreshToken 用于续签 AccessToken不暴露给前端,一般保存在 Cookie 或 HttpOnly


标准双 Token 模式提升了安全性和用户体验,避免因 AccessToken 频繁刷新带来的资源浪费。

特别讨论:表单静默超时的处理策略

场景问题:

用户长时间填写表单,没有发出任何请求,点击提交时发现 token 已失效,被重定向到登录页,数据全丢。

推荐方案:


提交失败后前端本地缓存表单数据;

登录成功后回显草稿,确保用户体验不受损;

或者在用户输入行为时定期心跳请求,触发后端续签。


总结

实现无感刷新 Token,是用户体验与安全性协同优化的重要实践。通过后端智能判断与前端拦截配合,结合双 Token 模式或动态续签机制,我们可以实现:

用户操作不中断 身份凭证自动续期 安全控制粒度更灵活


作者:星辰聊技术

链接:https://juejin.cn