一、双Token机制原理
1.1 基本概念
双Token机制是指同时使用Access Token和Refresh Token来实现认证和刷新:
![图片[1]_SpringBoot中双Token实现无感刷新方案_知途无界](https://zhituwujie.com/wp-content/uploads/2025/07/d2b5ca33bd20250710101841.png)
- Access Token:短期有效(通常30分钟-2小时),用于业务请求
- Refresh Token:长期有效(通常7天-30天),用于获取新Access Token
sequenceDiagram
participant Client
participant Server
Client->>Server: 登录请求(用户名+密码)
Server-->>Client: 返回accessToken+refreshToken
Client->>Server: 业务请求(带accessToken)
alt accessToken有效
Server-->>Client: 返回业务数据
else accessToken过期
Server-->>Client: 401 Unauthorized
Client->>Server: 使用refreshToken请求新accessToken
Server-->>Client: 返回新accessToken
Client->>Server: 重试原请求(带新accessToken)
Server-->>Client: 返回业务数据
end
1.2 优势分析
- 安全性:Access Token短期有效,即使泄露影响有限
- 用户体验:无感刷新避免频繁登录
- 灵活性:可独立控制两种Token的过期时间
二、SpringBoot实现方案
2.1 依赖配置
<!-- pom.xml -->
<dependencies>
<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>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>
2.2 JWT工具类
public class JwtUtil {
private static final String SECRET_KEY = "your-256-bit-secret";
private static final long ACCESS_EXPIRE = 30 * 60 * 1000; // 30分钟
private static final long REFRESH_EXPIRE = 7 * 24 * 60 * 60 * 1000; // 7天
// 生成accessToken
public static String generateAccessToken(String username) {
return Jwts.builder()
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + ACCESS_EXPIRE))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
// 生成refreshToken
public static String generateRefreshToken(String username) {
return Jwts.builder()
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + REFRESH_EXPIRE))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
// 验证并解析Token
public static Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
}
2.3 登录接口实现
@RestController
@RequestMapping("/auth")
public class AuthController {
@PostMapping("/login")
public ResponseEntity<Map<String, String>> login(@RequestBody LoginRequest request) {
// 1. 验证用户名密码(省略)
// 2. 生成双Token
String accessToken = JwtUtil.generateAccessToken(request.getUsername());
String refreshToken = JwtUtil.generateRefreshToken(request.getUsername());
// 3. 存储refreshToken(可存Redis或数据库)
redisTemplate.opsForValue().set(
"refresh:" + request.getUsername(),
refreshToken,
JwtUtil.REFRESH_EXPIRE,
TimeUnit.MILLISECONDS
);
// 4. 返回结果
Map<String, String> tokens = new HashMap<>();
tokens.put("accessToken", accessToken);
tokens.put("refreshToken", refreshToken);
return ResponseEntity.ok(tokens);
}
}
2.4 无感刷新实现
2.4.1 拦截器配置
public class TokenRefreshInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
try {
Claims claims = JwtUtil.parseToken(token);
// Token有效,放行
return true;
} catch (ExpiredJwtException e) {
// Access Token过期,尝试刷新
return refreshToken(request, response, e.getClaims().getSubject());
} catch (Exception e) {
// 其他异常
response.sendError(HttpStatus.UNAUTHORIZED.value(), "无效的Token");
return false;
}
}
private boolean refreshToken(HttpServletRequest request, HttpServletResponse response, String username) throws IOException {
String refreshToken = request.getHeader("Refresh-Token");
// 1. 验证refreshToken是否存在且有效
String storedRefreshToken = redisTemplate.opsForValue().get("refresh:" + username);
if (refreshToken == null || !refreshToken.equals(storedRefreshToken)) {
response.sendError(HttpStatus.UNAUTHORIZED.value(), "Refresh Token无效");
return false;
}
try {
Claims claims = JwtUtil.parseToken(refreshToken);
// 2. 生成新的accessToken
String newAccessToken = JwtUtil.generateAccessToken(username);
// 3. 返回新token给客户端
response.setHeader("New-Access-Token", newAccessToken);
return true;
} catch (Exception e) {
response.sendError(HttpStatus.UNAUTHORIZED.value(), "Refresh Token过期");
return false;
}
}
}
2.4.2 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TokenRefreshInterceptor())
.addPathPatterns("/api/**")
.excludePathPatterns("/auth/**");
}
}
三、前端配合实现
3.1 Axios请求拦截
// 创建axios实例
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 5000
})
// 请求拦截器
service.interceptors.request.use(
config => {
const accessToken = localStorage.getItem('accessToken')
if (accessToken) {
config.headers['Authorization'] = `Bearer ${accessToken}`
}
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
// 如果返回了新accessToken则更新
const newAccessToken = response.headers['new-access-token']
if (newAccessToken) {
localStorage.setItem('accessToken', newAccessToken)
}
return response.data
},
async error => {
const originalRequest = error.config
// 处理401错误且不是刷新token请求
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
try {
// 使用refreshToken获取新accessToken
const refreshToken = localStorage.getItem('refreshToken')
const res = await axios.post('/auth/refresh', { refreshToken })
// 存储新token
localStorage.setItem('accessToken', res.data.accessToken)
// 重试原始请求
originalRequest.headers['Authorization'] = `Bearer ${res.data.accessToken}`
return service(originalRequest)
} catch (err) {
// 刷新token失败,跳转登录
router.push('/login')
return Promise.reject(err)
}
}
return Promise.reject(error)
}
)
四、安全增强措施
4.1 Refresh Token保护
- 存储安全:
- 服务端:Redis存储,设置合理过期时间
- 客户端:HttpOnly Cookie或Secure LocalStorage
- 使用限制:
- 每个Refresh Token只能使用一次
- 使用后立即生成新Refresh Token(类似OAuth2机制)
4.2 黑名单机制
public class TokenBlacklist {
// 添加token到黑名单
public static void addToBlacklist(String token, long expireTime) {
redisTemplate.opsForValue().set(
"blacklist:" + token,
"1",
expireTime,
TimeUnit.MILLISECONDS
);
}
// 检查token是否在黑名单
public static boolean isBlacklisted(String token) {
return redisTemplate.hasKey("blacklist:" + token);
}
}
// 登出时调用
public void logout(String accessToken, String username) {
// 计算token剩余有效时间
long expireTime = JwtUtil.getExpireTime(accessToken) - System.currentTimeMillis();
if (expireTime > 0) {
TokenBlacklist.addToBlacklist(accessToken, expireTime);
}
// 删除refreshToken
redisTemplate.delete("refresh:" + username);
}
五、性能优化建议
- 减少Refresh Token验证开销:
- 使用Redis缓存验证结果(设置短时间缓存)
- 采用Bloom Filter预处理黑名单检查
- 并发请求处理:
// 使用ConcurrentHashMap防止并发刷新
private static final ConcurrentHashMap<String, String> REFRESHING_USERS = new ConcurrentHashMap<>();
boolean startRefresh(String username) {
return REFRESHING_USERS.putIfAbsent(username, "1") == null;
}
void endRefresh(String username) {
REFRESHING_USERS.remove(username);
}
- 监控与告警:
- 监控Token刷新频率异常
- 设置单用户刷新频率限制
六、完整流程图解
graph TD
A[用户登录] --> B[生成双Token]
B --> C[存储RefreshToken]
C --> D[返回双Token给客户端]
D --> E[客户端携带AccessToken请求]
E --> F{Token有效?}
F -->|是| G[处理业务请求]
F -->|否| H[检查RefreshToken]
H -->|有效| I[生成新AccessToken]
I --> J[返回新Token并处理原请求]
H -->|无效| K[返回401要求重新登录]
通过以上实现,SpringBoot应用可以构建一个安全、高效的无感刷新认证系统,既保证了用户体验,又确保了系统的安全性。实际应用中可根据业务需求调整Token过期时间和安全策略。
© 版权声明
文中内容均来源于公开资料,受限于信息的时效性和复杂性,可能存在误差或遗漏。我们已尽力确保内容的准确性,但对于因信息变更或错误导致的任何后果,本站不承担任何责任。如需引用本文内容,请注明出处并尊重原作者的版权。
THE END

























暂无评论内容