> 技术文档 > 前端与 Spring Boot 后端无感 Token 刷新 - 从原理到全栈实践

前端与 Spring Boot 后端无感 Token 刷新 - 从原理到全栈实践

在这里插入图片描述

🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志
🎐 个人CSND主页——Micro麦可乐的博客
🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战
🌺《RabbitMQ》专栏19年编写主要介绍使用JAVA开发RabbitMQ的系列教程,从基础知识到项目实战
🌸《设计模式》专栏以实际的生活场景为案例进行讲解,让大家对设计模式有一个更清晰的理解
🌛《开源项目》本专栏主要介绍目前热门的开源项目,带大家快速了解并轻松上手使用
🍎 《前端技术》专栏以实战为主介绍日常开发中前端应用的一些功能以及技巧,均附有完整的代码示例
✨《开发技巧》本专栏包含了各种系统的设计原理以及注意事项,并分享一些日常开发的功能小技巧
💕《Jenkins实战》专栏主要介绍Jenkins+Docker的实战教程,让你快速掌握项目CI/CD,是2024年最新的实战教程
🌞《Spring Boot》专栏主要介绍我们日常工作项目中经常应用到的功能以及技巧,代码样例完整
👍《Spring Security》专栏中我们将逐步深入Spring Security的各个技术细节,带你从入门到精通,全面掌握这一安全技术
如果文章能够给大家带来一定的帮助!欢迎关注、评论互动~

前端与 Spring Boot 后端无感 Token 刷新 - 从原理到全栈实践

  • 1. 前言
  • 2. 为什么要无感刷新
  • 3 无感刷新原理
      • 3.1 无感刷新流程
      • 3.2 关键技术点
  • 4、前端实现
  • 5. 后端实现
      • 5.1 基础依赖(pom.xml)
      • 5.2 数据库与实体(存储用户可选)
      • 5.3 Redis 存储 Refresh Token
      • 5.4 JWT 工具类
      • 5.5 刷新服务
      • 5.6 控制器Controller
      • 5.7 JWT 验证过滤器
  • 6. 结语

1. 前言

在我们前后端分离的应用中,常用的身份认证方案是基于 JWTJSON Web Token)。在保证安全性的同时,短生命周期的 Access Token 又会带来频繁登录的体验痛点。为了解决这个问题,我们引入 Refresh Token 并结合无感刷新机制,让客户端在 Access Token 过期时自动刷新,而无需用户手动重新登录,从而最大化提升用户体验。

小伙伴们可以通过本文,快速掌握无感 Token 刷新的原理以及实现方式


2. 为什么要无感刷新

在基于Token的用户认证系统中,通常会设计两种Token

Access Token:用于访问资源,有效期短(通常15-30分钟)
Refresh Token:用于获取新Access Token,有效期长(通常7天)

传统Token机制存在两大痛点:

频繁强制退出Access Token过期时用户需重新登录
安全隐患:延长Access Token有效期会增加安全风险

无感刷新解决了这些问题:

用户体验优先
Access Token 常设很短(如 5–15 分钟),若不自动刷新,登录态会频繁过期,用户被迫“重新登录”,体验极差

安全与性能平衡
短生命周期的 Access Token 能减少被截获滥用的风险
结合 Refresh Token(相对较长有效期),可以在安全与便捷间找到最佳点

前后端解耦
通过前端拦截器统一处理过期场景,无须在各业务请求中散落重复逻辑
后端专注提供刷新接口与失效策略,无需关心前端实现细节


3 无感刷新原理

3.1 无感刷新流程

前端与 Spring Boot 后端无感 Token 刷新 - 从原理到全栈实践

3.2 关键技术点

双 Token 机制

Access Token:短时有效,携带用户身份和权限
Refresh Token:长期有效,专用于换取新的 Access Token

拦截与重试

1、前端在每次 API 请求中携带 Access Token
2、若响应为 401 Unauthorized(或后端自定义过期码),前端拦截器自动调用刷新token接口,用 Refresh Token 获取新一对 Token;
3、获取成功后,前端重新发起失败的原始请求,用户无感知。

后端安全策略
Refresh Token 写入 Redis,并在刷新时做一次性或者滑动过期(可选)校验;
Refresh Token 刷新后失效,防止被盗用。


4、前端实现

下面以 Axios 为例演示拦截器逻辑。我们将 Tokens 保存在 localStorage 或者更安全的 [HttpOnly Cookie] 中(此处示例用 localStorage 方便演示)

// auth.jsimport axios from \'axios\';// Base Axios 实例const api = axios.create({ baseURL: \'/api\',});// Token 存取function getAccessToken() { return localStorage.getItem(\'access_token\'); }function getRefreshToken() { return localStorage.getItem(\'refresh_token\'); }function setTokens({ accessToken, refreshToken }) { localStorage.setItem(\'access_token\', accessToken); localStorage.setItem(\'refresh_token\', refreshToken);}// 请求拦截:自动附带 Access Tokenapi.interceptors.request.use(config => { const token = getAccessToken(); if (token) config.headers[\'Authorization\'] = `Bearer ${token}`; return config;});// 响应拦截:遇到 401 刷新并重试let isRefreshing = false;let subscribers = [];function onRefreshed(newToken) { subscribers.forEach(cb => cb(newToken)); subscribers = [];}function addSubscriber(cb) { subscribers.push(cb);}api.interceptors.response.use( res => res, error => { const { config, response } = error; if (response && response.status === 401 && !config._retry) { if (isRefreshing) { // 正在刷新,加入队列 return new Promise(resolve => { addSubscriber(token => { config.headers[\'Authorization\'] = `Bearer ${token}`; resolve(api(config)); }); }); } config._retry = true; isRefreshing = true; // 调用刷新接口 return api.post(\'/auth/refresh\', { refreshToken: getRefreshToken() }) .then(res => { const { accessToken, refreshToken } = res.data; setTokens({ accessToken, refreshToken }); isRefreshing = false; onRefreshed(accessToken); // 重试原请求 config.headers[\'Authorization\'] = `Bearer ${accessToken}`; return api(config); }) .catch(err => { // 刷新失败,跳转登录 isRefreshing = false; window.location.href = \'/login\'; return Promise.reject(err); }); } return Promise.reject(error); });export default api;

要点说明

isRefreshingsubscribers 用于解决多个并发 401 时只发送一次刷新请求;
_retry 标记避免无限循环;
刷新失败后,需清除本地登录态并跳转到登录页。


5. 后端实现

5.1 基础依赖(pom.xml)

<dependencies>  <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>  <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency>  <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>  <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>  <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency></dependencies>

5.2 数据库与实体(存储用户可选)

这里就简单模拟用户,仅有用户名和密码为例

-- 用户表(简化)CREATE TABLE user_account ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL);

5.3 Redis 存储 Refresh Token

我们用 ·Redis· 的 String,Key 为 refresh:{userId},Value 存 JSON { token, expireTime }

5.4 JWT 工具类

// JwtUtil.java@Componentpublic class JwtUtil { @Value(\"${jwt.secret}\") private String secret; @Value(\"${jwt.access.expire}\") private long accessExpire; // ms @Value(\"${jwt.refresh.expire}\") private long refreshExpire; // ms // 生成 Access Token(短期) public String generateAccessToken(Long userId) { return Jwts.builder() .setSubject(userId.toString()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + accessExpire)) .signWith(Keys.hmacShaKeyFor(secret.getBytes())) .compact(); } // 生成 Refresh Token(长期) public String generateRefreshToken(Long userId) { return Jwts.builder() .setSubject(userId.toString()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + refreshExpire)) .signWith(Keys.hmacShaKeyFor(secret.getBytes())) .compact(); } // 解析 Token public Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(secret.getBytes()) .build() .parseClaimsJws(token) .getBody(); }}

5.5 刷新服务

// AuthService.java@Servicepublic class AuthService { @Autowired private JwtUtil jwtUtil; @Autowired private StringRedisTemplate redis; public Tokens login(String username, String password) { // 1. 验证用户名密码(略,用 MyBatis-Plus 查询) Long userId = /* ... */; // 2. 生成双 Token String accessToken = jwtUtil.generateAccessToken(userId); String refreshToken = jwtUtil.generateRefreshToken(userId); // 3. 保存到 Redis String key = \"refresh:\" + userId; redis.opsForValue().set(key, refreshToken, jwtUtil.getRefreshExpire(), TimeUnit.MILLISECONDS); return new Tokens(accessToken, refreshToken); } public Tokens refresh(String refreshToken) { // 1. 解析 Claims claims = jwtUtil.parseToken(refreshToken); Long userId = Long.parseLong(claims.getSubject()); // 2. Redis 校验 String key = \"refresh:\" + userId; String cached = redis.opsForValue().get(key); if (cached == null || !cached.equals(refreshToken)) { throw new RuntimeException(\"Refresh Token 无效或已过期\"); } // 3. 生成新 Token String newAccess = jwtUtil.generateAccessToken(userId); String newRefresh = jwtUtil.generateRefreshToken(userId); // 4. 覆盖 Redis redis.opsForValue().set(key, newRefresh, jwtUtil.getRefreshExpire(), TimeUnit.MILLISECONDS); return new Tokens(newAccess, newRefresh); }}

5.6 控制器Controller

// AuthController.java@RestController@RequestMapping(\"/api/auth\")public class AuthController { @Autowired private AuthService authService; @PostMapping(\"/login\") public Tokens login(@RequestBody LoginReq req) { return authService.login(req.getUsername(), req.getPassword()); } @PostMapping(\"/refresh\") public Tokens refresh(@RequestBody Map<String,String> body) { return authService.refresh(body.get(\"refreshToken\")); }}// DTOs@Dataclass LoginReq { private String username, password; }@Data@AllArgsConstructorclass Tokens { private String accessToken; private String refreshToken; }

5.7 JWT 验证过滤器

由于验证并非本文的重点,小伙伴们可以参考博主的 《Spring Security》专栏学习,这里仅提供思路:
在每次请求拦截中,解析 Access Token 并将用户信息放入 SecurityContext,若过期则交由前端刷新逻辑处理。


6. 结语

本文详细介绍了 无感 Token 刷新 的核心原理,以及前端 Axios 拦截器与后端 Spring Boot + MyBatis-Plus + Redis 的完整示例代码。通过双 Token、Redis 校验与拦截重试,你可以在保证安全性的同时,给用户带来 无感登录过期刷新 的体验

后续可继续优化:

  • Refresh Token 滑动过期:每次刷新延长有效期;
  • Refresh Token 一次性使用:每个旧 Token 只能刷新一次;
  • 前端多 tab 协调:同域下可共享刷新状态,避免重复刷新;
  • 安全加固:结合 IP、UA 风控,防止 Token 被盗用。

希望本文能帮助你快速在项目中落地无感刷新方案,如果你在实践过程中有任何疑问或更好的扩展思路,欢迎在评论区留言,最后希望大家 一键三连 给博主一点点鼓励!


在这里插入图片描述