无感刷新 Token:打造“永不掉线”的丝滑用户体验
没有什么比在用户操作正嗨时,突然弹出“登录已过期,请重新登录”的提示更让人沮丧了。这种突如其来的中断不仅破坏了用户体验,还可能导致未保存的数据丢失。
然而,为了安全考虑,用于身份验证的 Access Token 必须设计成短生命周期。这就产生了一个矛盾:
- 安全性要求:Token 生命周期短,防止泄露带来长时间风险
- 用户体验要求:用户不希望频繁重新登录
解决这个矛盾的关键是 无感刷新机制(Silent Refresh Token),通过 Access Token + Refresh Token 的双 Token 体系,实现用户几乎感受不到的登录刷新体验。
1. 为什么需要 Refresh Token
在现代 Web 应用中,我们通常使用 JWT 或 Access Token 来验证每一次 API 请求。
Access Token 生命周期短(如 30 分钟到 1 小时),一旦泄露,攻击者可能利用它执行敏感操作。
问题:如果直接延长 Access Token 的有效期,安全性降低;如果保持短有效期,用户频繁掉线。
解决方案是引入 Refresh Token:
Token 类型 | 用途 | 特点 | 存储位置 |
---|---|---|---|
Access Token | 访问受保护资源 | 生命周期短、无状态 | 内存 / Vuex / Redux |
Refresh Token | 获取新的 Access Token | 生命周期长、有状态、可吊销 | HttpOnly Cookie(防 XSS) |
注意:Access Token 通常无状态,服务器无法主动吊销;Refresh Token 是有状态的,服务器可以在用户登出或修改密码时主动作废。
2. 无感刷新工作流程
完整流程如下:
首次登录
用户使用用户名和密码登录,服务器返回 Access Token + Refresh Token。正常请求
客户端将 Access Token 附加在请求头中访问 API。Access Token 过期
API 返回401 Unauthorized
。捕获 401 并触发刷新
客户端拦截器捕获 401 错误,并暂停失败请求。刷新 Access Token
使用 Refresh Token 调用/api/auth/refresh
接口:- 成功:返回新 Access Token(可选旋转 Refresh Token)
- 失败:返回错误(403 Forbidden),用户需要重新登录
重试或终结
- 刷新成功:自动重发失败请求,用户无感知
- 刷新失败:清除认证信息,重定向登录页面
3. 实战 Demo:Axios 拦截器实现无感刷新
以下示例展示了 处理并发请求、自动刷新 Token 并重试失败请求 的完整方案。
3.1 创建 Axios 实例
// api/request.js
import axios from 'axios';
import { refreshTokenApi } from './auth';
import { getAccessTokenFromStore, setAccessTokenInStore, logoutUser } from './authStore';
const service = axios.create({
baseURL: '/api',
timeout: 10000,
});
// 请求拦截器:每个请求自动带上 Access Token
service.interceptors.request.use(
config => {
const accessToken = getAccessTokenFromStore();
if (accessToken) {
config.headers['Authorization'] = `Bearer ${accessToken}`;
}
return config;
},
error => Promise.reject(error)
);
3.2 响应拦截器:处理无感刷新
let isRefreshing = false; // 刷新状态标志
let requestsQueue = []; // 挂起请求队列
service.interceptors.response.use(
response => response,
async error => {
const { config, response } = error;
const status = response?.status;
if (status !== 401) return Promise.reject(error);
// 如果正在刷新 Token,先挂起请求
if (isRefreshing) {
return new Promise(resolve => {
requestsQueue.push(() => resolve(service(config)));
});
}
isRefreshing = true;
try {
// 调用刷新 Token 接口
const { newAccessToken } = await refreshTokenApi();
// 更新本地 Access Token
setAccessTokenInStore(newAccessToken);
// 重试刚才失败的请求
config.headers['Authorization'] = `Bearer ${newAccessToken}`;
requestsQueue.forEach(cb => cb());
requestsQueue = [];
return service(config);
} catch (refreshError) {
console.error('Refresh Token 失效,请重新登录', refreshError);
logoutUser(); // 清理并重定向登录页
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
);
export default service;
3.3 核心要点解析
并发处理
isRefreshing
+requestsQueue
防止多个请求同时触发刷新,保证原子性。
无感刷新
- 用户操作不中断,失败请求会自动重试。
优雅降级
- Refresh Token 失效时自动登出,避免死循环或安全风险。
4. 总结
无感刷新 Token 是现代 Web 应用提升用户体验的 标配方案:
- 用户几乎感受不到登录过期
- API 安全性得到保障
- 支持并发请求,避免刷新竞态问题
- 支持 Refresh Token 旋转策略,提高安全性
实现这一机制不仅是几行代码,更是对 认证流程、安全性、用户体验 三者之间平衡的深刻理解。