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

双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 日志审计要点
# 成功刷新日志
[INFO] 用户[1001]通过设备[192.168.1.1|Chrome]刷新令牌,新有效期至2023-10-01 12:30

# 异常事件日志
[WARN] 检测到异常刷新请求,用户[1001]的设备[192.168.1.2|Firefox]与记录不匹配

7. 部署与回滚

7.1 分阶段部署
  1. Phase 1
    • 先部署新的Auth Service(含双Token接口)
    • 保持旧网关兼容两种令牌模式
  2. Phase 2
    • 部署新网关过滤器
    • 前端逐步灰度发布新逻辑
  3. Phase 3
    • 完全禁用旧令牌模式
    • 清理遗留的单一Token数据
7.2 回滚方案
  1. 紧急开关

    @Value("${security.token.mode:SINGLE}")
    private String tokenMode;
    
    public Mono<Void> filter(...) {
        if ("SINGLE".equals(tokenMode)) {
            // 回退到旧逻辑
        }
    }
    
  2. 数据兼容

    • 保持旧Token验证逻辑1周
    • 双写Refresh Token到新旧Redis结构

方案优势总结

  1. 安全性提升
    • Access Token短有效期降低泄露风险
    • Refresh Token通过HttpOnly Cookie保护
    • 设备指纹绑定防止跨设备滥用
  2. 用户体验优化
    • 无感知自动刷新机制
    • 支持多设备独立会话管理
  3. 系统扩展性
    • 易于实现令牌吊销列表(黑名单)
    • 支持细粒度权限变更实时生效
  4. 合规性保障
    • 符合OAuth 2.0规范
    • 满足GDPR等数据保护要求
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT枫斗者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值