在现代 Web 开发中,Token 认证 是最常见的身份验证方式之一。相比于传统的 Session 认证,Token 认证具有更高的安全性和可扩展性。而 双 Token 认证(Access Token + Refresh Token) 进一步解决了 Token 认证可能面临的安全问题,例如 Token 过期、频繁重新登录等。
本篇博客将详细介绍双 Token 认证的概念、工作流程、代码实现,并分析其优缺点。
一、为什么需要双 Token 认证?
单 Token 认证(通常是 JWT)存在几个主要问题:
-
Token 过期后需要重新登录
- 如果 Token 过期,用户需要重新输入用户名和密码登录,影响用户体验。
-
长期有效的 Token 安全性低
- 为了减少用户频繁登录,可以设定一个长期有效的 Token,但如果 Token 被盗,将导致安全隐患。
-
Token 不能轻易撤销
- 由于 JWT 是无状态的,一旦生成无法在服务器端主动失效,导致无法撤销已泄露的 Token。
双 Token 认证 通过 Access Token + Refresh Token 解决了这些问题:
- Access Token(短时令牌):
- 用于访问受保护的 API,生命周期较短(如 15~30 分钟)。
- Refresh Token(刷新令牌):
- 用于获取新的 Access Token,生命周期较长(如 7 天或 30 天)。
- 仅在服务器端存储并验证,不能用于访问 API。
二、双 Token 认证的工作流程
1. 用户登录并获取 Token
用户使用 用户名 + 密码 进行身份验证,服务器返回:
- 一个短期有效的 Access Token(用于访问 API)。
- 一个长期有效的 Refresh Token(用于获取新的 Access Token)。
客户端请求:
POST /api/auth/login
{
"username": "user1",
"password": "password123"
}
服务器返回:
{
"accessToken": "eyJhbGciOiJIUzI1NiIsIn...",
"refreshToken": "g1a2b3c4d5e6f7g8h9i0"
}
2. 访问受保护 API
- 客户端每次访问受保护的 API 时,都会携带 Access Token 作为
Authorization
头部。 - 服务器验证 Token 是否有效,如果有效,则返回数据,否则返回 401 未授权。
3. Access Token 过期,使用 Refresh Token 重新获取
- 如果 Access Token 过期,客户端使用 Refresh Token 请求新的 Access Token。
- 服务器验证 Refresh Token 的有效性:
- 有效:返回新的 Access Token,并可以选择返回新的 Refresh Token。
- 无效(如 Refresh Token 过期):需要重新登录。
POST /api/auth/refresh
{
"refreshToken": "g1a2b3c4d5e6f7g8h9i0"
}
服务器返回:
{
"accessToken": "new-access-token...",
"refreshToken": "new-refresh-token..."
}
三、代码实现(Spring Boot + JWT)
1. JWT工具类
import io.jsonwebtoken.*;
import java.util.Date;
public class JwtUtil {
private static final String SECRET_KEY = "my-secret-key";
private static final long ACCESS_TOKEN_EXPIRATION = 15 * 60 * 1000; // 15分钟
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.HS256, 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.HS256, SECRET_KEY)
.compact();
}
// 解析 Token
public static Claims parseToken(String token) throws ExpiredJwtException {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
}
2.用户登录,返回token
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
if (isValidUser(request.getUsername(), request.getPassword())) {
String accessToken = JwtUtil.generateAccessToken(request.getUsername());
String refreshToken = JwtUtil.generateRefreshToken(request.getUsername());
return ResponseEntity.ok(new AuthResponse(accessToken, refreshToken));
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials");
}
4.刷新token
@PostMapping("/refresh")
public ResponseEntity<?> refresh(@RequestBody RefreshRequest request) {
try {
Claims claims = JwtUtil.parseToken(request.getRefreshToken());
String newAccessToken = JwtUtil.generateAccessToken(claims.getSubject());
return ResponseEntity.ok(new AuthResponse(newAccessToken, request.getRefreshToken()));
} catch (ExpiredJwtException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Refresh Token 已过期");
}
}
四、双 Token 认证的优势
-
安全性更高
- Access Token 过期较快,即使被盗,影响范围有限。
- Refresh Token 仅存储在服务器端,不能直接访问 API,提高安全性。
-
提升用户体验
- Access Token 过期后,用户无需重新登录,只需使用 Refresh Token 自动刷新。
-
更灵活的 Token 管理
- 服务器可以在 Refresh Token 失效时让用户重新登录,以提高安全性。
五、总结
- Access Token 适用于短期访问,过期后需要使用 Refresh Token 获取新 Token。
- Refresh Token 只应存储在服务器端,避免暴露给前端。
- 双 Token 认证 提供更高的安全性,同时减少用户的重复登录,提高体验。
这种方式广泛应用于现代 Web 开发,尤其是 SPA(单页应用)、移动应用和微服务架构中。如果你的系统需要更高的安全性,建议结合 Redis 或数据库存储 Refresh Token 以便进行主动撤销。