ToB企服应用市场:ToB评测及商务社交产业平台

标题: 双Token机制(Access Token + Refresh Token)安全高效 [打印本页]

作者: 种地    时间: 昨天 10:05
标题: 双Token机制(Access Token + Refresh Token)安全高效
双Token机制(Access Token + Refresh Token)的具体实现步骤:
1. 令牌设计与生成

1.1 令牌界说


1.2 登录接口实现

  1. // AuthController.java
  2. @PostMapping("/login")
  3. public R<LoginResult> login(@RequestBody LoginRequest request) {
  4.     // 1. 验证用户密码
  5.     LoginUser user = remoteUserService.authenticate(request);
  6.    
  7.     // 2. 生成双Token
  8.     String accessToken = JwtUtils.generateAccessToken(user);
  9.     String refreshToken = UUID.randomUUID().toString();
  10.    
  11.     // 3. 存储Refresh Token到Redis(绑定设备和用户)
  12.     String deviceFingerprint = buildDeviceFingerprint(request);
  13.     String redisKey = buildRefreshTokenKey(user.getUserId(), deviceFingerprint);
  14.     redisService.setEx(redisKey, refreshToken, 7, TimeUnit.DAYS);
  15.    
  16.     // 4. 设置Refresh Token到Cookie
  17.     ResponseCookie cookie = ResponseCookie.from("refresh_token", refreshToken)
  18.         .httpOnly(true)
  19.         .secure(true)
  20.         .path("/")
  21.         .maxAge(7 * 24 * 3600)
  22.         .sameSite("Strict")
  23.         .build();
  24.    
  25.     return R.ok(new LoginResult(accessToken))
  26.         .addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
  27. }
复制代码

2. 令牌刷新接口

2.1 刷新端点实现

  1. // AuthController.java
  2. @PostMapping("/auth/refresh")
  3. public R<LoginResult> refreshToken(
  4.     @CookieValue(name = "refresh_token", required = false) String refreshToken,
  5.     HttpServletRequest request) {
  6.    
  7.     // 1. 验证Refresh Token存在性
  8.     if (StringUtils.isEmpty(refreshToken)) {
  9.         return R.fail(HttpStatus.UNAUTHORIZED, "缺少刷新令牌");
  10.     }
  11.    
  12.     // 2. 提取设备指纹
  13.     String deviceFingerprint = buildDeviceFingerprint(request);
  14.    
  15.     // 3. 查询Redis验证有效性
  16.     String redisKey = buildRefreshTokenKeyFromRequest(request); // 根据请求生成Key
  17.     String storedToken = redisService.get(redisKey);
  18.     if (!refreshToken.equals(storedToken)) {
  19.         return R.fail(HttpStatus.UNAUTHORIZED, "刷新令牌无效");
  20.     }
  21.    
  22.     // 4. 生成新Access Token
  23.     LoginUser user = getCurrentUser(); // 从上下文获取用户
  24.     String newAccessToken = JwtUtils.generateAccessToken(user);
  25.    
  26.     // 5. 可选:刷新Refresh Token有效期(滑动过期)
  27.     redisService.expire(redisKey, 7, TimeUnit.DAYS);
  28.    
  29.     return R.ok(new LoginResult(newAccessToken));
  30. }
复制代码
2.2 装备指纹生成逻辑

  1. private String buildDeviceFingerprint(HttpServletRequest request) {
  2.     String ip = ServletUtils.getClientIP(request);
  3.     String userAgent = request.getHeader("User-Agent");
  4.     return Hashing.sha256().hashString(ip + userAgent, StandardCharsets.UTF_8).toString();
  5. }
复制代码

3. 网关过滤器改造

3.1 验证流程调整

  1. // AuthFilter.java
  2. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  3.     ServerHttpRequest request = exchange.getRequest();
  4.    
  5.     // 1. 白名单直接放行
  6.     if (isIgnorePath(request.getPath().toString())) {
  7.         return chain.filter(exchange);
  8.     }
  9.    
  10.     // 2. 尝试获取Access Token
  11.     String accessToken = getAccessToken(request);
  12.    
  13.     try {
  14.         // 3. 验证Access Token有效性
  15.         Claims claims = JwtUtils.parseToken(accessToken);
  16.         if (claims != null && isTokenValid(claims)) {
  17.             // 正常流程
  18.             return chain.filter(addHeaders(exchange, claims));
  19.         }
  20.     } catch (ExpiredJwtException ex) {
  21.         // 4. Access Token过期,尝试刷新
  22.         return handleTokenRefresh(exchange, chain, ex.getClaims());
  23.     }
  24.    
  25.     // 5. 无有效令牌
  26.     return unauthorizedResponse(exchange, "请重新登录");
  27. }
  28. private Mono<Void> handleTokenRefresh(ServerWebExchange exchange,
  29.                                      GatewayFilterChain chain,
  30.                                      Claims expiredClaims) {
  31.     // 1. 获取Refresh Token
  32.     String refreshToken = getRefreshTokenFromCookie(exchange);
  33.    
  34.     // 2. 调用刷新接口(内部转发)
  35.     return WebClient.create()
  36.         .post()
  37.         .uri("http://auth-service/auth/refresh")
  38.         .cookie("refresh_token", refreshToken)
  39.         .retrieve()
  40.         .bodyToMono(R.class)
  41.         .flatMap(result -> {
  42.             if (result.getCode() == HttpStatus.SUCCESS) {
  43.                 // 3. 更新请求头中的Access Token
  44.                 String newToken = result.getData().get("accessToken");
  45.                 ServerHttpRequest newRequest = exchange.getRequest().mutate()
  46.                     .header("Authorization", "Bearer " + newToken)
  47.                     .build();
  48.                 return chain.filter(exchange.mutate().request(newRequest).build());
  49.             } else {
  50.                 return unauthorizedResponse(exchange, "会话已过期");
  51.             }
  52.         });
  53. }
复制代码

4. 安全增强步伐

4.1 Token绑定装备

  1. // JWT生成时加入设备指纹
  2. public static String generateAccessToken(LoginUser user, HttpServletRequest request) {
  3.     String fingerprint = buildDeviceFingerprint(request);
  4.     return Jwts.builder()
  5.         .setSubject(user.getUsername())
  6.         .claim("user_id", user.getUserId())
  7.         .claim("fp", fingerprint)
  8.         .setExpiration(new Date(System.currentTimeMillis() + 30 * 60 * 1000))
  9.         .signWith(SECRET_KEY)
  10.         .compact();
  11. }
  12. // 网关验证时检查设备
  13. private boolean validateDeviceFingerprint(Claims claims, HttpServletRequest request) {
  14.     String currentFp = buildDeviceFingerprint(request);
  15.     String tokenFp = claims.get("fp", String.class);
  16.     return currentFp.equals(tokenFp);
  17. }
复制代码
4.2 自动令牌撤销

  1. // 注销接口
  2. @PostMapping("/logout")
  3. public R<Void> logout(HttpServletRequest request) {
  4.     // 1. 获取当前设备指纹
  5.     String fingerprint = buildDeviceFingerprint(request);
  6.    
  7.     // 2. 删除Redis中的Refresh Token
  8.     String redisKey = buildRefreshTokenKey(getCurrentUserId(), fingerprint);
  9.     redisService.delete(redisKey);
  10.    
  11.     // 3. 将Access Token加入黑名单(剩余有效期内拒绝)
  12.     String accessToken = getAccessToken(request);
  13.     redisService.setEx("token_blacklist:" + accessToken, "1",
  14.         JwtUtils.getRemainingTime(accessToken), TimeUnit.SECONDS);
  15.    
  16.     // 4. 清除客户端Cookie
  17.     ResponseCookie cookie = ResponseCookie.from("refresh_token", "")
  18.         .maxAge(0)
  19.         .build();
  20.    
  21.     return R.ok().addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
  22. }
复制代码

5. 客户端实现示例

5.1 前端自动令牌管理

  1. // axios拦截器
  2. axios.interceptors.response.use(response => {
  3.   return response;
  4. }, error => {
  5.   const originalRequest = error.config;
  6.   
  7.   if (error.response?.status === 401 && !originalRequest._retry) {
  8.     originalRequest._retry = true;
  9.    
  10.     // 调用刷新接口
  11.     return axios.post('/auth/refresh', {}, { withCredentials: true })
  12.       .then(res => {
  13.         const newToken = res.data.accessToken;
  14.         localStorage.setItem('access_token', newToken);
  15.         originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
  16.         return axios(originalRequest);
  17.       });
  18.   }
  19.   return Promise.reject(error);
  20. });
复制代码
5.2 静默刷新机制

  1. // 定时检查Token有效期
  2. setInterval(() => {
  3.   const token = localStorage.getItem('access_token');
  4.   if (token && isTokenExpiringSoon(token)) { // 剩余<5分钟
  5.     axios.post('/auth/refresh', {}, { withCredentials: true })
  6.       .then(res => {
  7.         localStorage.setItem('access_token', res.data.accessToken);
  8.       });
  9.   }
  10. }, 300000); // 每5分钟检查
复制代码

6. 监控与运维

6.1 关键监控指标

指标名称监控方式报警阈值刷新令牌失败率Prometheus计数器>5% (连续5分钟)并发刷新冲突次数Redis分布式锁统计>10次/秒黑名单令牌数量Redis键空间统计突增50%时告警 6.2 日志审计要点

  1. # 成功刷新日志
  2. [INFO] 用户[1001]通过设备[192.168.1.1|Chrome]刷新令牌,新有效期至2023-10-01 12:30
  3. # 异常事件日志
  4. [WARN] 检测到异常刷新请求,用户[1001]的设备[192.168.1.2|Firefox]与记录不匹配
复制代码

7. 部署与回滚

7.1 分阶段部署

7.2 回滚方案


方案优势总结


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4