logo

双token与无感刷新:前端鉴权的进阶实践指南

作者:carzy2025.10.14 02:35浏览量:0

简介:本文详解双token机制及无感刷新token的实现原理,提供前后端完整代码示例与安全优化建议,助力开发者构建更安全的鉴权体系。

一、为何需要双token机制?

传统JWT鉴权存在两大痛点:其一,单token失效后需强制跳转登录页,用户体验割裂;其二,refresh_token若被窃取,攻击者可无限续期access_token。双token机制通过分离权限与身份信息,构建起更安全的防护体系。

1.1 传统方案的局限性

单token架构下,access_token过期时,客户端必须重新获取token。这种硬中断导致正在进行的操作(如支付流程)被迫终止,用户需手动重新登录。更严重的是,refresh_token通常设置较长有效期(如7天),一旦泄露将造成持久性安全风险。

1.2 双token的核心价值

双token机制将鉴权体系拆分为:

  • access_token:短期有效(如15分钟),携带用户权限信息
  • refresh_token:长期有效(如30天),仅用于身份验证

这种分离设计实现两大优化:权限变更时只需使access_token失效,不影响用户会话;refresh_token泄露风险降低,因其不包含权限数据。

二、双token实现原理详解

2.1 服务器端设计要点

2.1.1 数据库表结构

  1. CREATE TABLE user_tokens (
  2. id BIGSERIAL PRIMARY KEY,
  3. user_id BIGINT NOT NULL REFERENCES users(id),
  4. refresh_token VARCHAR(256) NOT NULL UNIQUE,
  5. expires_at TIMESTAMP NOT NULL,
  6. device_info VARCHAR(128),
  7. last_used_at TIMESTAMP
  8. );

关键字段说明:

  • refresh_token:采用HMAC-SHA256加密存储
  • device_info:记录设备指纹(IP+User-Agent哈希)
  • last_used_at:防重放攻击的时间戳

2.1.2 生成逻辑

  1. // Node.js示例
  2. const crypto = require('crypto');
  3. function generateTokens(userId) {
  4. const accessPayload = {
  5. sub: userId,
  6. exp: Math.floor(Date.now() / 1000) + 900, // 15分钟
  7. scope: 'read write'
  8. };
  9. const refreshPayload = {
  10. sub: userId,
  11. exp: Math.floor(Date.now() / 1000) + 2592000, // 30天
  12. device: getDeviceFingerprint()
  13. };
  14. const accessToken = jwt.sign(accessPayload, process.env.ACCESS_SECRET, { algorithm: 'HS256' });
  15. const refreshToken = crypto.randomBytes(32).toString('hex');
  16. // 存储refresh_token到数据库
  17. await db.query(
  18. 'INSERT INTO user_tokens(user_id, refresh_token, expires_at, device_info) VALUES($1, $2, $3, $4)',
  19. [userId, refreshToken, new Date(refreshPayload.exp * 1000), refreshPayload.device]
  20. );
  21. return { accessToken, refreshToken };
  22. }

2.2 客户端处理流程

2.2.1 存储策略

采用分层存储方案:

  • HttpOnly Cookie:存储refresh_token(防XSS)
  • 内存/SessionStorage:存储access_token(防CSRF需配合CSRF Token)

2.2.2 拦截器实现

  1. // Axios拦截器示例
  2. const api = axios.create();
  3. api.interceptors.response.use(
  4. response => response,
  5. async error => {
  6. const originalRequest = error.config;
  7. if (error.response.status === 401 && !originalRequest._retry) {
  8. originalRequest._retry = true;
  9. const refreshToken = getCookie('refresh_token');
  10. try {
  11. const response = await axios.post('/auth/refresh', { refreshToken });
  12. const { accessToken } = response.data;
  13. // 更新内存中的access_token
  14. storeAccessToken(accessToken);
  15. // 重试原始请求
  16. originalRequest.headers.Authorization = `Bearer ${accessToken}`;
  17. return api(originalRequest);
  18. } catch (refreshError) {
  19. // 刷新失败,跳转登录
  20. window.location.href = '/login';
  21. return Promise.reject(refreshError);
  22. }
  23. }
  24. return Promise.reject(error);
  25. }
  26. );

三、无感刷新实现技巧

3.1 提前刷新策略

在access_token剩余1/3寿命时触发刷新:

  1. function checkTokenExpiration() {
  2. const token = getAccessToken();
  3. if (!token) return;
  4. const payload = parseJwt(token);
  5. const now = Date.now() / 1000;
  6. const threshold = payload.exp * 0.7; // 剩余30%寿命时刷新
  7. if (now > threshold) {
  8. refreshAccessToken();
  9. }
  10. }
  11. // 每分钟检查一次
  12. setInterval(checkTokenExpiration, 60000);

3.2 并发请求处理

采用请求队列机制解决多个401并发问题:

  1. let isRefreshing = false;
  2. let subscribers = [];
  3. api.interceptors.response.use(
  4. response => response,
  5. error => {
  6. // ...前文401处理逻辑...
  7. if (error.response.status === 401) {
  8. if (!isRefreshing) {
  9. isRefreshing = true;
  10. refreshToken().then(newToken => {
  11. subscribers.forEach(cb => cb(newToken));
  12. subscribers = [];
  13. });
  14. }
  15. const retryRequest = new Promise(resolve => {
  16. subscribers.push(token => {
  17. originalRequest.headers.Authorization = `Bearer ${token}`;
  18. resolve(api(originalRequest));
  19. });
  20. });
  21. return retryRequest;
  22. }
  23. }
  24. );

四、安全增强方案

4.1 刷新令牌防护

  1. 设备绑定:验证refresh请求中的设备指纹
  2. 使用计数:限制refresh_token使用次数(如最多5次)
  3. 旋转机制:每次成功刷新后生成新的refresh_token

4.2 令牌吊销实现

  1. // Redis黑名单实现
  2. const redis = require('redis');
  3. const client = redis.createClient();
  4. async function revokeToken(token) {
  5. const payload = parseJwt(token);
  6. const key = `revoked:${payload.jti || payload.sub}`;
  7. await client.setEx(key, payload.exp - Date.now()/1000, 'true');
  8. }
  9. // 中间件验证
  10. async function validateToken(req, res, next) {
  11. const token = req.headers.authorization?.split(' ')[1];
  12. if (!token) return res.sendStatus(401);
  13. const payload = parseJwt(token);
  14. const isRevoked = await client.get(`revoked:${payload.jti || payload.sub}`);
  15. if (isRevoked) return res.sendStatus(401);
  16. next();
  17. }

五、最佳实践建议

  1. 短有效期策略:access_token建议5-15分钟,refresh_token建议1-30天
  2. HTTPS强制:所有token传输必须使用HTTPS
  3. CSRF防护:结合CSRF Token使用
  4. 监控告警:实时监控异常刷新行为
  5. 渐进式迁移:老系统可采用双token+旧session的混合模式过渡

六、常见问题解决方案

Q1:refresh_token被窃取怎么办?
A:实施设备指纹验证+IP地理围栏,发现异常立即吊销所有关联token。

Q2:如何处理移动端网络不稳定情况?
A:实现本地token缓存+离线模式,网络恢复后批量同步操作。

Q3:多标签页场景如何协调?
A:使用BroadcastChannel API或localStorage事件监听实现跨标签页通信。

通过双token机制与无感刷新技术的结合,开发者可在保障安全性的同时,提供接近无感知的用户体验。实际实施时需根据业务场景调整参数,并通过压力测试验证系统承载能力。

相关文章推荐

发表评论