简介:本文深入解析双token认证机制与无感刷新token的实现原理,通过代码示例和场景分析,帮助开发者快速掌握这一安全认证方案的核心要点。
传统单token认证存在两大痛点:有效期管理与安全性平衡。短期有效的token(如JWT)需频繁刷新,影响用户体验;长期有效的token则存在泄露风险。双token机制通过拆分权限与身份验证,实现了安全性与便利性的平衡。
典型交互流程:
CREATE TABLE user_tokens (id BIGSERIAL PRIMARY KEY,user_id BIGINT NOT NULL REFERENCES users(id),refresh_token VARCHAR(255) NOT NULL UNIQUE,expires_at TIMESTAMP NOT NULL,device_info JSONB -- 记录设备指纹等信息);
// 生成双tokenasync function generateTokens(userId) {const accessToken = jwt.sign({ userId, role: 'user' },ACCESS_SECRET,{ expiresIn: '15m' });const refreshToken = crypto.randomBytes(32).toString('hex');await db.query('INSERT INTO user_tokens(user_id, refresh_token, expires_at) VALUES($1, $2, $3)',[userId, refreshToken, new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)]);return { accessToken, refreshToken };}// 刷新tokenasync function refreshTokens(refreshToken) {const storedToken = await db.query('SELECT * FROM user_tokens WHERE refresh_token = $1',[refreshToken]);if (!storedToken || storedToken.expires_at < new Date()) {throw new Error('Invalid refresh token');}return generateTokens(storedToken.user_id);}
const authApi = {async callApi(endpoint, options) {try {const response = await fetch(endpoint, {...options,headers: {'Authorization': `Bearer ${localStorage.getItem('accessToken')}`}});if (response.status === 401) {await this.refreshToken();return this.callApi(endpoint, options); // 重试}return response;} catch (error) {if (error.message.includes('invalid_token')) {await this.refreshToken();return this.callApi(endpoint, options); // 重试}throw error;}},async refreshToken() {const refreshToken = await getRefreshToken(); // 从Cookie获取const response = await fetch('/api/refresh', {method: 'POST',body: JSON.stringify({ refreshToken })});if (response.ok) {const data = await response.json();localStorage.setItem('accessToken', data.accessToken);} else {// 处理刷新失败,跳转登录window.location.href = '/login';}}};
// 检查token剩余有效期function checkTokenExpiration() {const token = localStorage.getItem('accessToken');if (!token) return false;const decoded = jwtDecode(token);const expiry = decoded.exp * 1000; // 转换为毫秒const bufferTime = 5 * 60 * 1000; // 提前5分钟刷新return expiry - Date.now() < bufferTime;}// 在应用初始化时检查if (checkTokenExpiration()) {authApi.refreshToken();}
let isRefreshing = false;let subscribers = [];async function refreshToken() {if (isRefreshing) {return new Promise(resolve => {subscribers.push(resolve);});}isRefreshing = true;try {const response = await fetch('/api/refresh', {method: 'POST',credentials: 'include' // 获取Cookie中的refreshToken});const data = await response.json();localStorage.setItem('accessToken', data.accessToken);// 通知所有等待的请求subscribers.forEach(resolve => resolve(data.accessToken));subscribers = [];return data.accessToken;} finally {isRefreshing = false;}}
// 在JWT中添加jti(JWT ID)和iat(颁发时间)const accessToken = jwt.sign({userId,role: 'user',jti: crypto.randomBytes(16).toString('hex'),iat: Math.floor(Date.now() / 1000)},ACCESS_SECRET,{ expiresIn: '15m' });// 存储已使用的jti防止重放const usedJtis = new Set();// 中间件验证function validateJwt(req, res, next) {const token = req.headers.authorization?.split(' ')[1];if (!token) return res.status(401).send('Unauthorized');try {const decoded = jwt.verify(token, ACCESS_SECRET);if (usedJtis.has(decoded.jti)) {return res.status(401).send('Token reused');}usedJtis.add(decoded.jti);req.user = decoded;next();} catch (error) {res.status(401).send('Invalid token');}}
解决方案:使用BroadcastChannel API实现标签页间通信
// 在刷新token的标签页const channel = new BroadcastChannel('auth_channel');channel.postMessage({ type: 'token_refreshed', newToken: 'xxx' });// 在其他标签页const channel = new BroadcastChannel('auth_channel');channel.onmessage = (event) => {if (event.data.type === 'token_refreshed') {localStorage.setItem('accessToken', event.data.newToken);}};
优化策略:
// 服务端缓存最近使用的access tokenconst tokenCache = new LRUCache({ max: 1000, maxAge: 1000 * 60 * 5 }); // 5分钟缓存app.get('/api/data', (req, res) => {const token = req.headers.authorization?.split(' ')[1];if (tokenCache.has(token)) {// 从缓存获取用户信息return res.json(tokenCache.get(token));}// 正常验证流程...});
function calculateTokenExpiry(userId) {// 根据用户风险等级调整const riskLevel = await getUserRiskLevel(userId);switch(riskLevel) {case 'high': return '5m';case 'medium': return '15m';case 'low': return '1h';default: return '15m';}}
// 在API网关层记录请求模式const userBehavior = {lastActive: Date.now(),requestCount: 0};// 动态决定是否需要提前刷新function shouldEarlyRefresh(userId) {const behavior = getUserBehavior(userId);return behavior.requestCount > 50 &&(Date.now() - behavior.lastActive) < 30 * 60 * 1000;}
通过双token机制与无感刷新实现,可以在保证系统安全性的同时,显著提升用户体验。实际开发中应根据具体业务场景调整实现细节,并持续监控优化认证流程。