前端与 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 无感刷新流程

在这里插入图片描述

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;

要点说明

isRefreshingsubscribers 用于解决多个并发 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();}// 解析 Tokenpublic 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. 生成双 TokenString accessToken  = jwtUtil.generateAccessToken(userId);String refreshToken = jwtUtil.generateRefreshToken(userId);// 3. 保存到 RedisString 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. 生成新 TokenString newAccess  = jwtUtil.generateAccessToken(userId);String newRefresh = jwtUtil.generateRefreshToken(userId);// 4. 覆盖 Redisredis.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 被盗用。

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


在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/86138.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/86138.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【AI智能体】新手教程-通过 Chat SDK 搭建网页在线客服

通过扣子搭建的智能体可以一键发布为 Chat SDK&#xff0c;快速部署到你的自建网站中&#xff0c;作为在线智能客服面向网站的用户提供 AI 答疑服务。本文档介绍通过 Chat SDK 搭建网页版在线客服的详细操作步骤。 场景说明 网站作为企业和组织与用户互动的重要平台&#xff…

flask静态资源与模板页面、模板用户登录案例

案例代码 import flask# template_folder 模板文件夹(静态页面 html页面渲染) # static_folder 静态资源文件夹主要存放的是类似静态数据、音频、视频、图片等 app flask.Flask(__name__, static_folderstatic, template_foldertemplate)app.route(/) def index():# render_t…

【工具教程】识别PDF中文字内容,批量识别文字并保存到Excel表格中的操作步骤和方法

在日常办公和文件管理中&#xff0c;我们常常会遇到需要处理大量 PDF 文件的情况。有时&#xff0c;为了更好地管理和查找这些文件&#xff0c;需要根据 PDF 文件中特定区域的文字内容对文件进行重命名。例如&#xff0c;在企业档案管理中&#xff0c;合同文件可能需要根据合同…

重生学AI第十三集:初识神经网络之Conv2d

终于该学习神经网络的搭建了&#xff0c;开心&#xff0c;嘻嘻 学习神经网络离不开torch.nn&#xff0c;先把他印在脑子里&#xff0c;什么是torch.nn?他是Pytorch的一个模块&#xff0c;包含了大量构建神经网络需要的类和方法&#xff0c;就像前面学习的torch.utils&#xf…

学习C++、QT---07(C++的权限、C++的引用)

每日一言 你解决的每一个难题&#xff0c;都是在为未来的自己解锁新技能。 权限的讲解 这边呢我们利用银行的一个案例来讲解权限的奥秘 权限指的是public、private 、protected 就是这三种权限&#xff0c;因此有这一张表进行分清他们之间的区别和联系 但是我们在平时的话会因…

全球化短剧平台全栈技术架构白皮书:多区域部署、智能分发与沉浸式体验的完整解决方案

一、全球化基础架构深度设计 全球网络基础设施构建 采用多活数据中心部署模式&#xff0c;在北美&#xff08;弗吉尼亚&#xff09;、欧洲&#xff08;法兰克福&#xff09;、亚太&#xff08;新加坡&#xff09;建立三大核心枢纽节点 构建混合CDN网络&#xff0c;整合AWS Clo…

深入剖析 LGM—— 开启高分辨率 3D 内容创作新时代

一、引言 在当今数字化时代&#xff0c;3D 内容创作的需求如井喷般增长&#xff0c;从游戏开发中绚丽多彩的虚拟世界&#xff0c;到影视制作里震撼人心的特效场景&#xff0c;再到工业设计中精准无误的产品原型&#xff0c;3D 技术无处不在。然而&#xff0c;传统 3D 内容创作…

从用户到社区Committer:小米工程师隋亮亮的Apache Fory成长之路

Apache Fory 是一个基于JIT和零拷贝的高性能多语言序列化框架&#xff0c;实现了高效紧凑的序列化协议&#xff0c;提供极致的性能、压缩率和易用性。在多语言序列化框架技术领域取得了重大突破&#xff0c;推动序列化技术步入高性能易用新篇章&#xff01;这一切&#xff0c;都…

【Koa系列】10min快速入门Koa

简介 koa是基于node开发的一个服务端框架&#xff0c;功能同express&#xff0c;但更小巧简单。 官方仓库地址&#xff1a;https://github.com/koajs/koa 创建项目 创建文件夹nodeKoa&#xff0c;执行以下脚本 npm init -y npm i koa npm i nodemon 基础示例 创建一个服…

IDEA与通义联合:智能编程效率革命

IDEA与通义联合&#xff1a;智能编程效率革命 当最强Java IDE遇上顶尖AI助手&#xff0c;会碰撞出怎样的生产力火花&#xff1f; 思维导图解读&#xff1a;智能编程工作流 #mermaid-svg-uTAcSs1kBBmDwGfM {font-family:"trebuchet ms",verdana,arial,sans-serif;font…

Docker 数据持久化完全指南:Volume、Bind Mount 与匿名卷

Docker 数据持久化完全指南&#xff1a;Volume、Bind Mount 与匿名卷 引言 在 Docker 中&#xff0c;容器的文件系统默认是临时的&#xff0c;容器删除后数据也会丢失。为了实现数据持久化&#xff0c;Docker 提供了多种存储方式&#xff0c;主要包括&#xff1a; docker vo…

OSS跨区域复制灾备方案:华东1到华南1的数据同步与故障切换演练

1. 引言 对象存储服务&#xff08;OSS&#xff09;已成为现代数据架构的核心组件。随着业务全球化&#xff0c;跨区域数据灾备从“可选”变为“必选”。本文以阿里云OSS为实验环境&#xff0c;实战演练华东1&#xff08;杭州&#xff09;到华南1&#xff08;深圳&#xff09;的…

前端登录状态管理:主流方案对比与安全实践指南

根据目前业内前端登录状态管理的主流设计方案&#xff0c;及其演进趋势进行汇总&#xff0c;生成主要包括如下内容的报告&#xff1a; 登录状态保持的基础原理&#xff1a;从HTTP无状态问题出发解析技术需求&#xff0c;使用表格对比核心挑战。主流技术方案对比&#xff1a;详…

动手用 Web 实现一个 2048 游戏

文章目录 为什么选择 2048&#xff1f;关键技术点与算法详解HTML 结构&#xff1a;搭建游戏界面CSS 样式&#xff1a;美化游戏界面JavaScript 核心逻辑&#xff1a;驱动游戏运行1&#xff09;数据结构&#xff1a;二维数组表示游戏网格2&#xff09;核心算法&#xff1a;添加随…

frp v0.62.1内网穿透搭建和使用

官网&#xff1a;https://gofrp.org/zh-cn/ Github&#xff1a;https://github.com/fatedier/frp 开源项目 frp frp 是一种快速反向代理&#xff0c;允许您将位于 NAT 或防火墙后面的本地服务器公开给 Internet。目前支持 TCP 和 UDP&#xff0c;以及 HTTP 和 HTTPS 协议&…

如何使用 USB 数据线将文件从 PC 传输到 iPhone

虽然用 USB 数据线将文件从 PC 传输到安卓设备非常容易&#xff0c;但对于 iPhone 用户来说&#xff0c;情况就不同了。不过&#xff0c;幸运的是&#xff0c;我们找到了三种可靠的方法&#xff0c;可以使用 USB 数据线将文件从 PC 传输到 iPhone&#xff0c;让您轻松完成这项任…

【C++高阶三】AVL树深度剖析

【C高阶三】AVL树深度剖析 1.什么是AVL树2.AVL树的实现2.1节点类和基本结构2.2插入2.3旋转处理2.3.1左单旋2.3.2右单旋2.3.3左右双旋2.3.4右左双旋 1.什么是AVL树 AVL树也叫二叉搜索平衡树 因为二叉搜索树如果插入顺序是有序的&#xff0c;那么这棵树的查找效率将会是O(N)&…

LangChain 文本分割器深度解析:从原理到落地应用(上)

食用指南 LangChain 作为大语言模型应用开发框架&#xff0c;文本分割器是其核心组件之一&#xff0c;本文以此作为切入点&#xff0c;详细介绍文本分割的作用、策略、以及常见的文本切割器应用。考虑到篇幅过长&#xff0c;故拆分为上、中、下三篇&#xff0c;后续会在中篇介…

【Java高频面试问题】高并发篇

【Java高频面试问题】高并发篇 Kafka原理核心组件高吞吐核心机制高可用设计 Kafka 如何保证消息不丢失如何解决Kafka重复消费一、生产者端&#xff1a;根源防重二、消费者端&#xff1a;精准控制三、业务层&#xff1a;幂等性设计&#xff08;核心方案&#xff09; 如何解决Kaf…

关于结构体,排序,递推的详细讲解(从属于GESP四级)

本章内容 排序算法基础 结构体 递推 简单双指针 一、排序算法基础三剑客 冒泡 Bubble、选择 Selection、插入 Insertion 1. 预备知识 1.1 排序算法评价指标 指标 含义 影响答题的典型问法 时间复杂度 算法在最坏、平均或最好情况下所需比较 / 交换次数 “写出此算法…