种地 发表于 2025-3-3 10:05:25

双Token机制(Access Token + Refresh Token)安全高效

双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 日志审计要点

# 成功刷新日志
用户通过设备刷新令牌,新有效期至2023-10-01 12:30

# 异常事件日志
检测到异常刷新请求,用户的设备与记录不匹配
7. 部署与回滚

7.1 分阶段部署


[*] Phase 1:

[*]先部署新的Auth Service(含双Token接口)
[*]保持旧网关兼容两种令牌模式

[*] Phase 2:

[*]部署新网关过滤器
[*]前端逐步灰度发布新逻辑

[*] Phase 3:

[*]完全禁用旧令牌模式
[*]清理遗留的单一Token数据

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企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 双Token机制(Access Token + Refresh Token)安全高效