token无感刷新全流程
demo代码:Arata08/tokenDemo

Java (Spring Boot) 和 Vue (配合 Axios) 前端的无感刷新全流程:
方案:
- 双 Token 机制: 服务器同时签发两个 Token:
access_token: 短时效,用于日常接口访问鉴权。refresh_token: 长时效,非常安全,仅用于获取新的access_token。
- 前端拦截器: Vue 前端使用 Axios 拦截器,在每个请求头中自动添加
access_token。 - 后端校验: Java 后端拦截器或过滤器校验
access_token的有效性。 - 前端感知与刷新:
- 当后端返回特定状态码(如
401 Unauthorized)表示access_token过期时。 - 前端不直接跳转登录页,而是发起一个专用的刷新 Token 接口请求,并携带
refresh_token。 - 后端验证
refresh_token,如果有效,则生成新的access_token(有时也更新refresh_token)并返回给前端。 - 前端收到新的
access_token后,更新本地存储,并重试之前失败的请求。
- 当后端返回特定状态码(如
流程总结
- 用户登录成功,后端返回
access_token和refresh_token,前端存储。 - 前端每次 API 请求时,通过 Axios 请求拦截器自动在
Authorization头中添加Bearer access_token。 - 后端通过过滤器验证
access_token。 - 当
access_token过期,后端返回 401。 - 前端 Axios 响应拦截器捕获 401 错误。
- 拦截器发起
/auth/refresh请求,携带refresh_token。 - 后端验证
refresh_token,成功则返回新的access_token。 - 前端更新本地
access_token。 - 拦截器自动重试之前失败的请求(使用新
access_token)。 - 如果
refresh_token也失败或不存在,则清除本地 Token,跳转登录页。
后端 (Java - Spring Boot) 实现
登录接口: 登录成功后返回 access_token 和 refresh_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;
