跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Java大前端java

前端与 Spring Boot 后端无感 Token 刷新实践

前后端分离架构中 JWT 认证常因 Access Token 短有效期导致频繁登录。本方案采用双 Token 机制,Access Token 用于资源访问,Refresh Token 用于续期。前端通过 Axios 拦截器捕获 401 状态码自动调用刷新接口,后端利用 Redis 存储 Refresh Token 并校验有效性。该实现平衡了安全与体验,支持 Spring Boot 与主流前端框架集成,无需用户手动操作即可完成 Token 无感刷新。

CryptoLab发布于 2026/4/12更新于 2026/9/1070 浏览
前端与 Spring Boot 后端无感 Token 刷新实践

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

流程图

1. 前言

在前后端分离的应用中,常用的身份认证方案是基于 JWT(JSON Web Token)。在保证安全性的同时,短生命周期的 Access Token 会带来频繁登录的体验痛点。为了解决这个问题,我们引入 Refresh Token 并结合无感刷新机制,让客户端在 Access 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 无感刷新流程

流程图

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.js
import 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 Token
api.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;

要点说明

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

5. 后端实现

5.1 基础依赖(pom.xml)

<dependencies>
  <!-- Spring Boot Starter Web -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <!-- MyBatis-Plus -->
  <dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
  </dependency>
  <!-- MySQL 驱动 -->
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
  </dependency>
  <!-- Redis -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
  </dependency>
  <!-- JWT -->
  <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
@Component
public 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
@Service
public 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
@Data
class LoginReq {
    private String username, password;
}

@Data
@AllArgsConstructor
class 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 被盗用。

目录

  1. 前端与 Spring Boot 后端无感 Token 刷新 - 从原理到全栈实践
  2. 1. 前言
  3. 2. 为什么要无感刷新
  4. 3. 无感刷新原理
  5. 3.1 无感刷新流程
  6. 3.2 关键技术点
  7. 4. 前端实现
  8. 5. 后端实现
  9. 5.1 基础依赖(pom.xml)
  10. 5.2 数据库与实体(存储用户可选)
  11. 5.3 Redis 存储 Refresh Token
  12. 5.4 JWT 工具类
  13. 5.5 刷新服务
  14. 5.6 控制器 Controller
  15. 5.7 JWT 验证过滤器
  16. 6. 结语

更多推荐文章

查看全部
  • Hadoop MapReduce 数据排序实战:正序与逆序实现
  • WebPShop 插件:让 Photoshop 完美支持 WebP 格式
  • WebRTC + Java 信令服务器 + Vue 实时视频聊天实现
  • Nginx 与 Web 安全加固
  • 多模态模型开发实战:文本、图像与语音融合指南
  • 智能家居联动与节能优化:Java大数据实战拆解
  • 反无人机技术原理与反制手段详解
  • 基于 DeepSeek 和 Cursor 构建智能代码审查工具实战
  • SpringBoot 整合 Neo4j 图数据库实战指南
  • Python 中的 == 与 is:本质区别与最佳实践
  • Windows 7 编译 llama.cpp 运行 Qwen3 模型指南
  • Code Llama 7B 快速上手指南
  • 大模型学习路线:从零基础到精通的进阶指南
  • 基于高阶控制障碍函数的端到端无人机高速避障方案
  • MySQL 核心语法与实战基础
  • LocalAI:无需显卡即可在本地运行开源大模型
  • C++ 栈模拟验证栈序列:LeetCode 946 题解
  • AirSim 无人机仿真入门:实现起飞与降落
  • Cursor Rules 配置参考:前后端 Golang/TypeScript/Kotlin 开发规范
  • Openclaw 连接本地 Ollama 无响应排查指南

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online