双token与无感刷新:前端鉴权的进阶实践指南
2025.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 数据库表结构
CREATE TABLE user_tokens (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
refresh_token VARCHAR(256) NOT NULL UNIQUE,
expires_at TIMESTAMP NOT NULL,
device_info VARCHAR(128),
last_used_at TIMESTAMP
);
关键字段说明:
refresh_token
:采用HMAC-SHA256加密存储device_info
:记录设备指纹(IP+User-Agent哈希)last_used_at
:防重放攻击的时间戳
2.1.2 生成逻辑
// Node.js示例
const crypto = require('crypto');
function generateTokens(userId) {
const accessPayload = {
sub: userId,
exp: Math.floor(Date.now() / 1000) + 900, // 15分钟
scope: 'read write'
};
const refreshPayload = {
sub: userId,
exp: Math.floor(Date.now() / 1000) + 2592000, // 30天
device: getDeviceFingerprint()
};
const accessToken = jwt.sign(accessPayload, process.env.ACCESS_SECRET, { algorithm: 'HS256' });
const refreshToken = crypto.randomBytes(32).toString('hex');
// 存储refresh_token到数据库
await db.query(
'INSERT INTO user_tokens(user_id, refresh_token, expires_at, device_info) VALUES($1, $2, $3, $4)',
[userId, refreshToken, new Date(refreshPayload.exp * 1000), refreshPayload.device]
);
return { accessToken, refreshToken };
}
2.2 客户端处理流程
2.2.1 存储策略
采用分层存储方案:
- HttpOnly Cookie:存储refresh_token(防XSS)
- 内存/SessionStorage:存储access_token(防CSRF需配合CSRF Token)
2.2.2 拦截器实现
// Axios拦截器示例
const api = axios.create();
api.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const refreshToken = getCookie('refresh_token');
try {
const response = await axios.post('/auth/refresh', { refreshToken });
const { accessToken } = response.data;
// 更新内存中的access_token
storeAccessToken(accessToken);
// 重试原始请求
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return api(originalRequest);
} catch (refreshError) {
// 刷新失败,跳转登录
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
三、无感刷新实现技巧
3.1 提前刷新策略
在access_token剩余1/3寿命时触发刷新:
function checkTokenExpiration() {
const token = getAccessToken();
if (!token) return;
const payload = parseJwt(token);
const now = Date.now() / 1000;
const threshold = payload.exp * 0.7; // 剩余30%寿命时刷新
if (now > threshold) {
refreshAccessToken();
}
}
// 每分钟检查一次
setInterval(checkTokenExpiration, 60000);
3.2 并发请求处理
采用请求队列机制解决多个401并发问题:
let isRefreshing = false;
let subscribers = [];
api.interceptors.response.use(
response => response,
error => {
// ...前文401处理逻辑...
if (error.response.status === 401) {
if (!isRefreshing) {
isRefreshing = true;
refreshToken().then(newToken => {
subscribers.forEach(cb => cb(newToken));
subscribers = [];
});
}
const retryRequest = new Promise(resolve => {
subscribers.push(token => {
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(api(originalRequest));
});
});
return retryRequest;
}
}
);
四、安全增强方案
4.1 刷新令牌防护
- 设备绑定:验证refresh请求中的设备指纹
- 使用计数:限制refresh_token使用次数(如最多5次)
- 旋转机制:每次成功刷新后生成新的refresh_token
4.2 令牌吊销实现
// Redis黑名单实现
const redis = require('redis');
const client = redis.createClient();
async function revokeToken(token) {
const payload = parseJwt(token);
const key = `revoked:${payload.jti || payload.sub}`;
await client.setEx(key, payload.exp - Date.now()/1000, 'true');
}
// 中间件验证
async function validateToken(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.sendStatus(401);
const payload = parseJwt(token);
const isRevoked = await client.get(`revoked:${payload.jti || payload.sub}`);
if (isRevoked) return res.sendStatus(401);
next();
}
五、最佳实践建议
- 短有效期策略:access_token建议5-15分钟,refresh_token建议1-30天
- HTTPS强制:所有token传输必须使用HTTPS
- CSRF防护:结合CSRF Token使用
- 监控告警:实时监控异常刷新行为
- 渐进式迁移:老系统可采用双token+旧session的混合模式过渡
六、常见问题解决方案
Q1:refresh_token被窃取怎么办?
A:实施设备指纹验证+IP地理围栏,发现异常立即吊销所有关联token。
Q2:如何处理移动端网络不稳定情况?
A:实现本地token缓存+离线模式,网络恢复后批量同步操作。
Q3:多标签页场景如何协调?
A:使用BroadcastChannel API或localStorage事件监听实现跨标签页通信。
通过双token机制与无感刷新技术的结合,开发者可在保障安全性的同时,提供接近无感知的用户体验。实际实施时需根据业务场景调整参数,并通过压力测试验证系统承载能力。
发表评论
登录后可评论,请前往 登录 或 注册