概述
使用 JWT + 双 Token 的无感刷新机制,主要是通过短期的 Access Token 和长期的 Refresh Token 相结合,保证系统的安全性和用户体验。其核心思想是:当 Access Token 过期时,使用 Refresh Token 无需用户再次登录即可获取新的 Access Token,从而实现无感刷新。
实现思路
- Access Token:短期有效的 Token,主要用于鉴权,每次请求时都需要携带,过期时间较短,通常为几分钟到数小时。
- Refresh Token:长期有效的 Token,仅用于刷新 Access Token,有效期较长,通常为几天到几周,不应该频繁使用,只有当 Access Token 过期时才使用。
- 无感刷新:当 Access Token 过期时,客户端检测到 401 错误后,自动发送 Refresh Token 到服务器获取新的 Access Token,用户无感知。
实现步骤
添加依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
生成 Access Token 和 Refresh Token
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
public class JwtTokenUtil {
private static final String SECRET_KEY = "your_secret_key"; // 密钥,生产中应存储在安全地方
private static final long ACCESS_TOKEN_EXPIRATION = 5 * 60 * 1000; // 5分钟
private static final long REFRESH_TOKEN_EXPIRATION = 7 * 24 * 60 * 60 * 1000; // 7天
// 生成 Access Token
public static String generateAccessToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRATION))
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
}
// 生成 Refresh Token
public static String generateRefreshToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRATION))
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
}
// 验证 Token 是否过期
public static boolean isTokenExpired(String token) {
try {
Date expiration = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody().getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return true;
}
}
// 从 Token 中获取用户名
public static String getUsernameFromToken(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody().getSubject();
}
}
实现刷新 Token 逻辑
在用户登录时,后端会生成并返回 Access Token 和 Refresh Token。当 Access Token 过期时,客户端可以使用 Refresh Token 进行无感刷新。
@RestController
@RequestMapping("/auth")
public class AuthController {
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
// 验证用户登录逻辑
String username = loginRequest.getUsername();
// 生成 Access Token 和 Refresh Token
String accessToken = JwtTokenUtil.generateAccessToken(username);
String refreshToken = JwtTokenUtil.generateRefreshToken(username);
// 返回给客户端
Map<String, String> tokens = new HashMap<>();
tokens.put("accessToken", accessToken);
tokens.put("refreshToken", refreshToken);
return ResponseEntity.ok(tokens);
}
@PostMapping("/refresh-token")
public ResponseEntity<?> refreshAccessToken(@RequestBody RefreshTokenRequest refreshTokenRequest) {
String refreshToken = refreshTokenRequest.getRefreshToken();
// 验证 Refresh Token 是否有效
if (JwtTokenUtil.isTokenExpired(refreshToken)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Refresh token expired");
}
// 通过 Refresh Token 获取用户名,并生成新的 Access Token
String username = JwtTokenUtil.getUsernameFromToken(refreshToken);
String newAccessToken = JwtTokenUtil.generateAccessToken(username);
Map<String, String> tokens = new HashMap<>();
tokens.put("accessToken", newAccessToken);
return ResponseEntity.ok(tokens);
}
}
前端实现无感刷新逻辑
前端在请求时需要携带 Access Token。当检测到 Access Token 过期时(如返回 401 错误),可以使用 Refresh Token 请求新的 Access Token,然后重试失败的请求。
import axios from 'axios';
// 获取 Token
let accessToken = localStorage.getItem('accessToken');
let refreshToken = localStorage.getItem('refreshToken');
// 创建 axios 实例
const api = axios.create({
baseURL: 'http://localhost:8080', // 后端地址
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
// 请求拦截器
api.interceptors.request.use(
(config) => {
config.headers['Authorization'] = `Bearer ${accessToken}`;
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
api.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// 用 Refresh Token 获取新 Access Token
const response = await axios.post('http://localhost:8080/auth/refresh-token', {
refreshToken: refreshToken,
});
// 更新 Access Token
accessToken = response.data.accessToken;
localStorage.setItem('accessToken', accessToken);
// 重新尝试原来的请求
originalRequest.headers['Authorization'] = `Bearer ${accessToken}`;
return api(originalRequest);
} catch (refreshError) {
// Refresh Token 过期或无效时需要重新登录
console.error('Refresh token expired or invalid', refreshError);
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
// 示例 API 调用
api.get('/some-protected-resource')
.then((response) => {
console.log(response.data);
})
.catch((error) => {
console.error(error);
});
总结
通过 JWT + 双 Token 实现无感刷新,可以提高系统的安全性,并且提升用户体验,减少频繁登录的麻烦。在实现中:
- Access Token 用于快速鉴权,有效期短;
- Refresh Token 用于刷新 Access Token,有效期长,使用频率低;
- 前端通过拦截响应中的 401 错误,实现无感刷新;