当前位置: 首页 > news >正文

token无感刷新全流程

demo代码:Arata08/tokenDemo

Java (Spring Boot) 和 Vue (配合 Axios) 前端的无感刷新全流程:

方案:

  1. 双 Token 机制: 服务器同时签发两个 Token:
    • access_token: 短时效,用于日常接口访问鉴权。
    • refresh_token: 长时效,非常安全,仅用于获取新的 access_token
  2. 前端拦截器: Vue 前端使用 Axios 拦截器,在每个请求头中自动添加 access_token
  3. 后端校验: Java 后端拦截器或过滤器校验 access_token 的有效性。
  4. 前端感知与刷新:
    • 当后端返回特定状态码(如 401 Unauthorized)表示 access_token 过期时。
    • 前端不直接跳转登录页,而是发起一个专用的刷新 Token 接口请求,并携带 refresh_token
    • 后端验证 refresh_token,如果有效,则生成新的 access_token(有时也更新 refresh_token)并返回给前端。
    • 前端收到新的 access_token 后,更新本地存储,并重试之前失败的请求。

流程总结

  1. 用户登录成功,后端返回 access_tokenrefresh_token,前端存储。
  2. 前端每次 API 请求时,通过 Axios 请求拦截器自动在 Authorization 头中添加 Bearer access_token
  3. 后端通过过滤器验证 access_token
  4. access_token 过期,后端返回 401。
  5. 前端 Axios 响应拦截器捕获 401 错误。
  6. 拦截器发起 /auth/refresh 请求,携带 refresh_token
  7. 后端验证 refresh_token,成功则返回新的 access_token
  8. 前端更新本地 access_token
  9. 拦截器自动重试之前失败的请求(使用新 access_token)。
  10. 如果 refresh_token 也失败或不存在,则清除本地 Token,跳转登录页。

后端 (Java - Spring Boot) 实现

登录接口: 登录成功后返回 access_tokenrefresh_token

@RestController
@RequestMapping("/api/auth")
public class AuthController {@Autowiredprivate JwtUtil jwtUtil;// 模拟用户验证逻辑private boolean validateUser(String username, String password) {// 实际应用中应查询数据库return "admin".equals(username) && "password".equals(password);}@PostMapping("/login")public ResponseEntity<Map<String, String>> login(@RequestBody Map<String, String> loginRequest) {String username = loginRequest.get("username");String password = loginRequest.get("password");if (validateUser(username, password)) {String accessToken = jwtUtil.generateAccessToken(username);String refreshToken = jwtUtil.generateRefreshToken(username);Map<String, String> response = new HashMap<>();response.put("access_token", accessToken);response.put("refresh_token", refreshToken);response.put("token_type", "Bearer");return ResponseEntity.ok(response);} else {return ResponseEntity.status(401).body(null); // 或返回错误信息}}
}

刷新 Token 接口: 接收 refresh_token,验证后返回新的 access_token

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate JwtUtil jwtUtil;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {String authHeader = request.getHeader("Authorization");if (authHeader != null && authHeader.startsWith("Bearer ")) {String token = authHeader.substring(7); // 移除 "Bearer " 前缀try {Claims claims = jwtUtil.parseAccessToken(token);// 可以将用户信息存入 SecurityContext 或 Request AttributesString username = claims.getSubject();// SecurityContextHolder.getContext().setAuthentication(...);request.setAttribute("currentUser", username);} catch (RuntimeException e) {// Token 无效,返回 401response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.getWriter().write("{\"error\":\"Unauthorized\"}");return; // 不继续执行后续过滤器/控制器}}// 如果没有 Token 或 Token 有效,继续执行filterChain.doFilter(request, response);}
}

Token 工具类: 用于生成、解析、验证 JWT

@Component
public class JwtUtil {// 注意:生产中应从配置文件读取,并且密钥要足够复杂且保密private final String ACCESS_TOKEN_SECRET = "your_very_strong_access_token_secret_here";private final String REFRESH_TOKEN_SECRET = "your_very_strong_refresh_token_secret_here";private final long ACCESS_TOKEN_EXPIRATION = 30 * 60 * 1000; // 30分钟private final long REFRESH_TOKEN_EXPIRATION = 7 * 24 * 60 * 60 * 1000; // 7天private SecretKey getAccessTokenKey() {return Keys.hmacShaKeyFor(ACCESS_TOKEN_SECRET.getBytes());}private SecretKey getRefreshTokenKey() {return Keys.hmacShaKeyFor(REFRESH_TOKEN_SECRET.getBytes());}// 生成 Access Tokenpublic String generateAccessToken(String username) {Date now = new Date();Date expiryDate = new Date(now.getTime() + ACCESS_TOKEN_EXPIRATION);return Jwts.builder().setSubject(username).setIssuedAt(new Date()).setExpiration(expiryDate).signWith(getAccessTokenKey()).compact();}// 生成 Refresh Tokenpublic String generateRefreshToken(String username) {Date now = new Date();Date expiryDate = new Date(now.getTime() + REFRESH_TOKEN_EXPIRATION);return Jwts.builder().setSubject(username).setIssuedAt(new Date()).setExpiration(expiryDate).signWith(getRefreshTokenKey()).compact();}// 解析 Access Tokenpublic Claims parseAccessToken(String token) {try {return Jwts.parserBuilder().setSigningKey(getAccessTokenKey()).build().parseClaimsJws(token).getBody();} catch (JwtException | IllegalArgumentException e) {// Token 无效或过期throw new RuntimeException("Invalid or expired access token", e);}}// 解析 Refresh Tokenpublic Claims parseRefreshToken(String token) {try {return Jwts.parserBuilder().setSigningKey(getRefreshTokenKey()).build().parseClaimsJws(token).getBody();} catch (JwtException | IllegalArgumentException e) {// Token 无效或过期throw new RuntimeException("Invalid or expired refresh token", e);}}// 验证 Access Token 是否过期public boolean isAccessTokenExpired(String token) {try {Claims claims = parseAccessToken(token);Date expiration = claims.getExpiration();return expiration.before(new Date());} catch (JwtException e) {return true; // 解析失败也视为过期}}
}

JWT 拦截器/过滤器: 在访问需要认证的接口前,验证 access_token

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate JwtUtil jwtUtil;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {String authHeader = request.getHeader("Authorization");if (authHeader != null && authHeader.startsWith("Bearer ")) {String token = authHeader.substring(7); // 移除 "Bearer " 前缀try {Claims claims = jwtUtil.parseAccessToken(token);// 可以将用户信息存入 SecurityContext 或 Request AttributesString username = claims.getSubject();// SecurityContextHolder.getContext().setAuthentication(...);request.setAttribute("currentUser", username);} catch (RuntimeException e) {// Token 无效,返回 401response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.getWriter().write("{\"error\":\"Unauthorized\"}");return; // 不继续执行后续过滤器/控制器}}// 如果没有 Token 或 Token 有效,继续执行filterChain.doFilter(request, response);}
}

前端 (Vue - Axios) 实现

Axios 配置: 创建 Axios 实例,并配置请求和响应拦截器。

import axios from 'axios';const API_BASE_URL = 'http://localhost:8080/api'; // 替换为你的后端地址const apiClient = axios.create({baseURL: API_BASE_URL,timeout: 10000,
});// 请求拦截器:在每个请求头中添加 access_token
apiClient.interceptors.request.use((config) => {const accessToken = localStorage.getItem('access_token'); // 或 sessionStorageif (accessToken) {config.headers.Authorization = `Bearer ${accessToken}`;}return config;},(error) => {return Promise.reject(error);}
);let isRefreshing = false;
let failedQueue = [];const processQueue = (error, token = null) => {failedQueue.forEach(prom => {if (error) {prom.reject(error);} else {prom.resolve(token);}});failedQueue = [];
};// 响应拦截器:处理 401 错误并尝试刷新 token
apiClient.interceptors.response.use((response) => {// 对响应数据做点什么return response;},async (error) => {const originalRequest = error.config;if (error.response?.status === 401 && !originalRequest._retry) {if (isRefreshing) {// 如果正在刷新,则将请求加入队列等待return new Promise((resolve, reject) => {failedQueue.push({ resolve, reject });}).then(token => {originalRequest.headers['Authorization'] = 'Bearer ' + token;return apiClient(originalRequest);}).catch(err => {return Promise.reject(err);});}originalRequest._retry = true; // 标记为已重试,防止无限循环isRefreshing = true;const refreshToken = localStorage.getItem('refresh_token'); // 或 sessionStorageif (!refreshToken) {// 如果没有 refresh_token,直接跳转登录// router.push('/login');// 或者触发登出逻辑localStorage.removeItem('access_token');localStorage.removeItem('refresh_token');window.location.href = '/login'; // 简单示例return Promise.reject(error);}try {const refreshResponse = await apiClient.post('/auth/refresh', {}, {headers: {Authorization: `Bearer ${refreshToken}`,}});const newAccessToken = refreshResponse.data.access_token;// const newRefreshToken = refreshResponse.data.refresh_token; // 如果后端也返回了新的 refresh_token// 更新本地存储的 tokenlocalStorage.setItem('access_token', newAccessToken);// localStorage.setItem('refresh_token', newRefreshToken); // 如果更新了// 更新正在请求的 axios 实例的 headerapiClient.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`;// 重试原始请求originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;processQueue(null, newAccessToken);return apiClient(originalRequest);} catch (refreshError) {console.error('Token refresh failed:', refreshError);processQueue(refreshError, null);// Refresh token 也失败了,登出用户localStorage.removeItem('access_token');localStorage.removeItem('refresh_token');window.location.href = '/login'; // 简单示例return Promise.reject(refreshError);} finally {isRefreshing = false;}}return Promise.reject(error);}
);export default apiClient;
    http://www.dtcms.com/a/609658.html

    相关文章:

  1. MySQL 数据增删改查
  2. 浏阳做网站的公司价格网站设计步骤详解
  3. 南京做网站外包试论述网上商城的推广技巧
  4. 面试150——二叉树
  5. opencv 学习: QA_02 什么是图像中的高频成分和低频成分
  6. C++_面试题13_QVector和QList的区别
  7. Vue 2脚手架从入门到实战核心知识点全解析(day6):从工程结构到高级通信(附代码讲解)
  8. 2025年AI面试防作弊指南:技术笔试如何识别异常行为
  9. (十)嵌入式面试题收集:15道
  10. 标准解读|即将实施的三份汽车安全强制性标准
  11. 手机网站建设的流程2024房价即将暴涨十大城市
  12. 根系扫描仪实战解析:如何精准获取根长、根表面积与拓扑结构?
  13. Mock技术文档
  14. 【OpenCV + VS】用addWeighted实现图像线性融合
  15. Ubuntu系统创建Conda环境并安装Pytorch(保姆教程)
  16. 腾讯KaLM-Embedding开源,登顶全球第一
  17. 从零开始学习tensort模型部署(二):从文件加载引擎的完整指南
  18. Muon 优化器代码实现详解
  19. 老河口网站设计保定网站搜索引擎优化
  20. 基于TRAESOLO与cpolar的AI远程开发环境搭建教程
  21. 一个公司可以做几个网站吗icp许可证
  22. 引入日志系统设计:基于UDP协议的 回声系统 服务器-客户端通信实现
  23. Shell 文件查找与复制
  24. 网站服务器免费申请北京市住房和城乡建设官网
  25. wordpress全站模板学校网站对学校建设的重要性
  26. 小波自适应去噪在脑电信号处理MATLAB仿真实现
  27. conda安装Django+pg运行环境
  28. 【淘店CRM电商管理分享】以我开源的安心转支付宝批量转账工具为例,浅谈程序员软件产品变现的路径,剖析一款成熟软件产品的运营策略
  29. Vue浅响应式如何解决深层响应式的性能问题?适用场景有哪些?
  30. 如何使用Metasploit进行暴力破解的详细步骤