在Java应用中实现无感Token刷新的核心原理是:当Access Token过期时,通过Refresh Token自动获取新Token并重试请求,用户全程无感知。以下是详细实现方案:
核心原理
-
双Token机制:
-
Access Token:短期有效(如2小时),用于请求鉴权
-
Refresh Token:长期有效(如7天),用于刷新Access Token
-
-
无感刷新流程:
-
请求接口返回
401/403
时触发Token刷新 -
用Refresh Token获取新Access Token
-
自动重试原始请求
-
若Refresh Token过期则强制退出登录
-
Java实现(基于Spring Boot + JWT)
1. 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
2. JWT工具类
public class JwtUtils {
private static final String SECRET_KEY = "your-secret-key";
private static final long ACCESS_EXPIRE = 30 * 60 * 1000; // 30分钟
private static final long REFRESH_EXPIRE = 7 * 24 * 60 * 60 * 1000; // 7天
// 生成双Token
public static Map<String, String> generateTokens(String username) {
String accessToken = Jwts.builder()
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + ACCESS_EXPIRE))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
String refreshToken = Jwts.builder()
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + REFRESH_EXPIRE))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
return Map.of("accessToken", accessToken, "refreshToken", refreshToken);
}
// 刷新Access Token
public static String refreshAccessToken(String refreshToken) throws ExpiredJwtException {
Claims claims = parseToken(refreshToken);
return Jwts.builder()
.setSubject(claims.getSubject())
.setExpiration(new Date(System.currentTimeMillis() + ACCESS_EXPIRE))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
// 解析Token
public static Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
}
3. 实现拦截器处理无感刷新
@Component
public class TokenRefreshInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService; // 包含刷新Token的业务方法
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String accessToken = request.getHeader("Authorization");
try {
JwtUtils.parseToken(accessToken); // 验证Access Token
return true;
} catch (ExpiredJwtException ex) {
// Access Token过期时尝试刷新
String refreshToken = request.getHeader("Refresh-Token");
if (refreshToken != null) {
try {
// 刷新Token并更新响应头
String newAccessToken = userService.refreshToken(refreshToken);
response.setHeader("New-Access-Token", newAccessToken);
return true;
} catch (Exception e) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false; // 刷新失败
}
}
}
return false;
}
}
4. 前端配合实现自动重试
// 使用axios拦截器
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// 用Refresh Token获取新Access Token
const newToken = await refreshToken();
// 更新请求头并重试
originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
return axios(originalRequest);
}
return Promise.reject(error);
}
);
关键注意事项
-
安全存储:
-
Refresh Token必须存储在
HttpOnly Cookie
中,防止XSS攻击 -
Access Token建议存内存或SessionStorage
-
-
并发控制:
-
当多个请求同时触发刷新时,需用锁机制保证只刷新一次
-
-
Refresh Token轮换:
-
每次刷新后生成新的Refresh Token,使旧Token立即失效
-
-
失效处理:
-
Refresh Token过期需清空客户端Token并跳转登录页
-