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。
java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private 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。
java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Override
protected 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 Attributes
String username = claims.getSubject();
// SecurityContextHolder.getContext().setAuthentication(...);
request.setAttribute("currentUser", username);
} catch (RuntimeException e) {
// Token 无效,返回 401
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"error\":\"Unauthorized\"}");
return; // 不继续执行后续过滤器/控制器
}
}
// 如果没有 Token 或 Token 有效,继续执行
filterChain.doFilter(request, response);
}
}
Token 工具类: 用于生成、解析、验证 JWT
java
@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 Token
public 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 Token
public 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 Token
public 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 Token
public 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
java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Override
protected 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 Attributes
String username = claims.getSubject();
// SecurityContextHolder.getContext().setAuthentication(...);
request.setAttribute("currentUser", username);
} catch (RuntimeException e) {
// Token 无效,返回 401
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"error\":\"Unauthorized\"}");
return; // 不继续执行后续过滤器/控制器
}
}
// 如果没有 Token 或 Token 有效,继续执行
filterChain.doFilter(request, response);
}
}
前端 (Vue - Axios) 实现
Axios 配置: 创建 Axios 实例,并配置请求和响应拦截器。
javascript
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'); // 或 sessionStorage
if (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'); // 或 sessionStorage
if (!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
// 更新本地存储的 token
localStorage.setItem('access_token', newAccessToken);
// localStorage.setItem('refresh_token', newRefreshToken); // 如果更新了
// 更新正在请求的 axios 实例的 header
apiClient.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;