双Token机制(Access Token + Refresh Token)的具体实现步骤:
1. 令牌设计与生成
1.1 令牌界说
- Access Token
- 有效期:30分钟(短效)
- 存储方式:客户端内存或非长期化存储(如JavaScript变量)
- 内容:用户ID、权限范围、装备指纹哈希、签发时间
- 格式:JWT(含exp声明)
- Refresh Token
- 有效期:7天(长效)
- 存储方式:HttpOnly + Secure Cookie(防XSS)
- 内容:全局唯一标识符(UUID)、用户ID、装备指纹哈希
- 格式:不透明字符串(存储于Redis)
1.2 登录接口实现
- // AuthController.java
- @PostMapping("/login")
- public R<LoginResult> login(@RequestBody LoginRequest request) {
- // 1. 验证用户密码
- LoginUser user = remoteUserService.authenticate(request);
-
- // 2. 生成双Token
- String accessToken = JwtUtils.generateAccessToken(user);
- String refreshToken = UUID.randomUUID().toString();
-
- // 3. 存储Refresh Token到Redis(绑定设备和用户)
- String deviceFingerprint = buildDeviceFingerprint(request);
- String redisKey = buildRefreshTokenKey(user.getUserId(), deviceFingerprint);
- redisService.setEx(redisKey, refreshToken, 7, TimeUnit.DAYS);
-
- // 4. 设置Refresh Token到Cookie
- ResponseCookie cookie = ResponseCookie.from("refresh_token", refreshToken)
- .httpOnly(true)
- .secure(true)
- .path("/")
- .maxAge(7 * 24 * 3600)
- .sameSite("Strict")
- .build();
-
- return R.ok(new LoginResult(accessToken))
- .addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
- }
复制代码 2. 令牌刷新接口
2.1 刷新端点实现
- // AuthController.java
- @PostMapping("/auth/refresh")
- public R<LoginResult> refreshToken(
- @CookieValue(name = "refresh_token", required = false) String refreshToken,
- HttpServletRequest request) {
-
- // 1. 验证Refresh Token存在性
- if (StringUtils.isEmpty(refreshToken)) {
- return R.fail(HttpStatus.UNAUTHORIZED, "缺少刷新令牌");
- }
-
- // 2. 提取设备指纹
- String deviceFingerprint = buildDeviceFingerprint(request);
-
- // 3. 查询Redis验证有效性
- String redisKey = buildRefreshTokenKeyFromRequest(request); // 根据请求生成Key
- String storedToken = redisService.get(redisKey);
- if (!refreshToken.equals(storedToken)) {
- return R.fail(HttpStatus.UNAUTHORIZED, "刷新令牌无效");
- }
-
- // 4. 生成新Access Token
- LoginUser user = getCurrentUser(); // 从上下文获取用户
- String newAccessToken = JwtUtils.generateAccessToken(user);
-
- // 5. 可选:刷新Refresh Token有效期(滑动过期)
- redisService.expire(redisKey, 7, TimeUnit.DAYS);
-
- return R.ok(new LoginResult(newAccessToken));
- }
复制代码 2.2 装备指纹生成逻辑
- private String buildDeviceFingerprint(HttpServletRequest request) {
- String ip = ServletUtils.getClientIP(request);
- String userAgent = request.getHeader("User-Agent");
- return Hashing.sha256().hashString(ip + userAgent, StandardCharsets.UTF_8).toString();
- }
复制代码 3. 网关过滤器改造
3.1 验证流程调整
- // AuthFilter.java
- public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
- ServerHttpRequest request = exchange.getRequest();
-
- // 1. 白名单直接放行
- if (isIgnorePath(request.getPath().toString())) {
- return chain.filter(exchange);
- }
-
- // 2. 尝试获取Access Token
- String accessToken = getAccessToken(request);
-
- try {
- // 3. 验证Access Token有效性
- Claims claims = JwtUtils.parseToken(accessToken);
- if (claims != null && isTokenValid(claims)) {
- // 正常流程
- return chain.filter(addHeaders(exchange, claims));
- }
- } catch (ExpiredJwtException ex) {
- // 4. Access Token过期,尝试刷新
- return handleTokenRefresh(exchange, chain, ex.getClaims());
- }
-
- // 5. 无有效令牌
- return unauthorizedResponse(exchange, "请重新登录");
- }
- private Mono<Void> handleTokenRefresh(ServerWebExchange exchange,
- GatewayFilterChain chain,
- Claims expiredClaims) {
- // 1. 获取Refresh Token
- String refreshToken = getRefreshTokenFromCookie(exchange);
-
- // 2. 调用刷新接口(内部转发)
- return WebClient.create()
- .post()
- .uri("http://auth-service/auth/refresh")
- .cookie("refresh_token", refreshToken)
- .retrieve()
- .bodyToMono(R.class)
- .flatMap(result -> {
- if (result.getCode() == HttpStatus.SUCCESS) {
- // 3. 更新请求头中的Access Token
- String newToken = result.getData().get("accessToken");
- ServerHttpRequest newRequest = exchange.getRequest().mutate()
- .header("Authorization", "Bearer " + newToken)
- .build();
- return chain.filter(exchange.mutate().request(newRequest).build());
- } else {
- return unauthorizedResponse(exchange, "会话已过期");
- }
- });
- }
复制代码 4. 安全增强步伐
4.1 Token绑定装备
- // JWT生成时加入设备指纹
- public static String generateAccessToken(LoginUser user, HttpServletRequest request) {
- String fingerprint = buildDeviceFingerprint(request);
- return Jwts.builder()
- .setSubject(user.getUsername())
- .claim("user_id", user.getUserId())
- .claim("fp", fingerprint)
- .setExpiration(new Date(System.currentTimeMillis() + 30 * 60 * 1000))
- .signWith(SECRET_KEY)
- .compact();
- }
- // 网关验证时检查设备
- private boolean validateDeviceFingerprint(Claims claims, HttpServletRequest request) {
- String currentFp = buildDeviceFingerprint(request);
- String tokenFp = claims.get("fp", String.class);
- return currentFp.equals(tokenFp);
- }
复制代码 4.2 自动令牌撤销
- // 注销接口
- @PostMapping("/logout")
- public R<Void> logout(HttpServletRequest request) {
- // 1. 获取当前设备指纹
- String fingerprint = buildDeviceFingerprint(request);
-
- // 2. 删除Redis中的Refresh Token
- String redisKey = buildRefreshTokenKey(getCurrentUserId(), fingerprint);
- redisService.delete(redisKey);
-
- // 3. 将Access Token加入黑名单(剩余有效期内拒绝)
- String accessToken = getAccessToken(request);
- redisService.setEx("token_blacklist:" + accessToken, "1",
- JwtUtils.getRemainingTime(accessToken), TimeUnit.SECONDS);
-
- // 4. 清除客户端Cookie
- ResponseCookie cookie = ResponseCookie.from("refresh_token", "")
- .maxAge(0)
- .build();
-
- return R.ok().addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
- }
复制代码 5. 客户端实现示例
5.1 前端自动令牌管理
- // axios拦截器
- axios.interceptors.response.use(response => {
- return response;
- }, error => {
- const originalRequest = error.config;
-
- if (error.response?.status === 401 && !originalRequest._retry) {
- originalRequest._retry = true;
-
- // 调用刷新接口
- return axios.post('/auth/refresh', {}, { withCredentials: true })
- .then(res => {
- const newToken = res.data.accessToken;
- localStorage.setItem('access_token', newToken);
- originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
- return axios(originalRequest);
- });
- }
- return Promise.reject(error);
- });
复制代码 5.2 静默刷新机制
- // 定时检查Token有效期
- setInterval(() => {
- const token = localStorage.getItem('access_token');
- if (token && isTokenExpiringSoon(token)) { // 剩余<5分钟
- axios.post('/auth/refresh', {}, { withCredentials: true })
- .then(res => {
- localStorage.setItem('access_token', res.data.accessToken);
- });
- }
- }, 300000); // 每5分钟检查
复制代码 6. 监控与运维
6.1 关键监控指标
指标名称监控方式报警阈值刷新令牌失败率Prometheus计数器>5% (连续5分钟)并发刷新冲突次数Redis分布式锁统计>10次/秒黑名单令牌数量Redis键空间统计突增50%时告警 6.2 日志审计要点
- # 成功刷新日志
- [INFO] 用户[1001]通过设备[192.168.1.1|Chrome]刷新令牌,新有效期至2023-10-01 12:30
- # 异常事件日志
- [WARN] 检测到异常刷新请求,用户[1001]的设备[192.168.1.2|Firefox]与记录不匹配
复制代码 7. 部署与回滚
7.1 分阶段部署
- Phase 1:
- 先部署新的Auth Service(含双Token接口)
- 保持旧网关兼容两种令牌模式
- Phase 2:
- Phase 3:
7.2 回滚方案
- 紧急开关:
- @Value("${security.token.mode:SINGLE}")
- private String tokenMode;
- public Mono<Void> filter(...) {
- if ("SINGLE".equals(tokenMode)) {
- // 回退到旧逻辑
- }
- }
复制代码 - 数据兼容:
- 保持旧Token验证逻辑1周
- 双写Refresh Token到新旧Redis布局
方案优势总结
- 安全性提升:
- Access Token短有效期降低泄露风险
- Refresh Token通过HttpOnly Cookie保护
- 装备指纹绑定防止跨装备滥用
- 用户体验优化:
- 系统扩展性:
- 易于实现令牌吊销列表(黑名单)
- 支持细粒度权限变更实时生效
- 合规性保障:
- 符合OAuth 2.0规范
- 满足GDPR等数据保护要求
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |