告别登录过期!无感刷新Token实战指南,让用户体验丝般顺滑
深入剖析双Token认证系统,用Axios拦截器实现无缝身份验证
引言:为何登录总是突然过期?
想象一下这样的场景:你正在精心编辑一份重要文档,全神贯注地输入内容,突然一个弹窗出现——"登录已过期,请重新登录"。不仅操作被中断,未保存的数据也可能丢失。这种糟糕的体验背后,其实是安全性与用户体验之间的天然矛盾。
问题的根源:Access Token的两难境地
Access Token的"天生矛盾"
- 安全性要求:Access Token需要有较短的有效期(通常30分钟到1小时),防止泄露后被长期滥用
- 用户体验要求:用户不希望频繁被强制重新登录,期望持续的工作流程
解决方案:双Token认证系统
为了解决这一矛盾,现代身份验证系统引入了两种Token:
Token类型 | 用途 | 特点 | 存储位置 |
---|---|---|---|
Access Token | 访问受保护API资源 | 生命周期短(1小时),无状态 | 客户端内存(Vuex/Redux) |
Refresh Token | 获取新的Access Token | 生命周期长(7-30天),有状态 | HttpOnly Cookie |
为何不直接使用长有效期的Token?
- 安全风险:长有效期Token一旦泄露,攻击者可在很长时间内冒充用户
- 无法主动吊销:JWT通常无状态,服务器无法主动使其失效
- 精细化控制:Refresh Token可被服务器管理,支持主动登出和设备管理
无感刷新的工作原理
核心工作流程
- 首次登录:用户凭密码登录,获取Access Token和Refresh Token
- 正常请求:API请求携带Access Token
- Token过期:服务器返回401 Unauthorized
- 拦截错误:客户端拦截401错误,暂停失败请求
- 发起刷新:使用Refresh Token获取新Access Token
- 处理结果:
- 成功:用新Token重试原请求,用户无感知
- 失败:清除认证信息,引导用户重新登录
安全增强:刷新令牌旋转
为提高安全性,建议在每次刷新时同时更新Refresh Token:
- 服务器使旧Refresh Token失效
- 返回新的Access Token和Refresh Token
- 有效防止Refresh Token被重用
实战实现:Axios拦截器方案
1. 创建Axios实例
// api/request.js
import axios from 'axios';
const service = axios.create({
baseURL: '/api',
timeout: 10000,
});
// 请求拦截器:自动添加Token
service.interceptors.request.use(
config => {
const accessToken = getAccessToken(); // 从存储中获取
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
error => Promise.reject(error)
);
2. 核心响应拦截器实现
// api/request.js
let isRefreshing = false; // 刷新状态标志
let requests = []; // 等待队列
service.interceptors.response.use(
response => response,
async error => {
const { config, response } = error;
// 非401错误直接返回
if (!response || response.status !== 401) {
return Promise.reject(error);
}
// 防止重复刷新
if (isRefreshing) {
return new Promise(resolve => {
requests.push(() => resolve(service(config)));
});
}
isRefreshing = true;
try {
// 尝试刷新Token
const { accessToken, refreshToken } = await refreshToken();
// 更新存储的Token
setAccessToken(accessToken);
setRefreshToken(refreshToken);
// 重试原请求
config.headers.Authorization = `Bearer ${accessToken}`;
// 执行等待队列中的所有请求
requests.forEach(cb => cb());
requests = [];
return service(config);
} catch (refreshError) {
// 刷新失败,清除认证并跳转登录
clearAuth();
redirectToLogin();
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
);
export default service;
3. Token管理函数
// utils/auth.js
// 获取Access Token
export const getAccessToken = () => {
return localStorage.getItem('access_token');
};
// 设置Access Token
export const setAccessToken = (token) => {
localStorage.setItem('access_token', token);
};
// 获取Refresh Token (通常由HttpOnly Cookie自动管理)
export const getRefreshToken = async () => {
// 在实际项目中,Refresh Token通常通过HttpOnly Cookie自动发送
// 这里无需手动处理
};
// 刷新Token的API调用
export const refreshToken = async () => {
const response = await axios.post('/api/auth/refresh', {}, {
withCredentials: true // 确保发送Cookie
});
return response.data;
};
// 清除认证信息
export const clearAuth = () => {
localStorage.removeItem('access_token');
// 服务器端会使Refresh Token失效
};
高级优化策略
1. 预防性刷新
在Token即将过期前主动刷新,避免等待401错误:
// utils/tokenMonitor.js
let refreshTimeout = null;
export const startTokenMonitor = () => {
const token = getAccessToken();
if (!token) return;
const payload = JSON.parse(atob(token.split('.')[1]));
const expiresAt = payload.exp * 1000;
const now = Date.now();
const bufferTime = 5 * 60 * 1000; // 提前5分钟刷新
// 清除现有定时器
if (refreshTimeout) {
clearTimeout(refreshTimeout);
}
// 设置刷新定时器
const refreshTime = expiresAt - now - bufferTime;
if (refreshTime > 0) {
refreshTimeout = setTimeout(() => {
refreshToken().then(({ accessToken }) => {
setAccessToken(accessToken);
startTokenMonitor(); // 重新启动监控
});
}, refreshTime);
}
};
2. 错误处理与重试机制
// 增强版错误处理
service.interceptors.response.use(
response => response,
async error => {
const { config, response } = error;
if (response.status === 401) {
// 处理401错误(如上文实现)
} else if (response.status >= 500) {
// 服务器错误,实现指数退避重试
if (!config.retryCount) {
config.retryCount = 0;
}
if (config.retryCount < 3) {
config.retryCount++;
const delay = Math.pow(2, config.retryCount) * 1000;
return new Promise(resolve => {
setTimeout(() => resolve(service(config)), delay);
});
}
}
return Promise.reject(error);
}
);
安全最佳实践
- HttpOnly Cookie:Refresh Token必须通过HttpOnly Cookie存储,防止XSS攻击
- SameSite属性:设置Cookie的SameSite=Strict防止CSRF攻击
- 令牌黑名单:服务器维护失效Token列表,支持主动登出
- 短期有效期:Access Token保持短期有效(建议≤1小时)
- 使用HTTPS:所有认证相关请求必须使用HTTPS
测试策略
为确保无感刷新机制可靠工作,需要测试以下场景:
- 正常流程:Token过期后自动刷新并重试请求
- 并发请求:多个请求同时遇到401错误时的处理
- 刷新失败:Refresh Token过期时的登出行为
- 网络异常:刷新过程中网络中断的应对
// 测试示例:模拟Token过期
describe('Token Refresh', () => {
it('should refresh token and retry request when receiving 401', async () => {
// 模拟首次请求返回401
mockServer.onGet('/api/data').replyOnce(401);
// 模拟刷新接口成功
mockServer.onPost('/api/auth/refresh').reply(200, {
accessToken: 'new-token',
refreshToken: 'new-refresh-token'
});
// 模拟重试请求成功
mockServer.onGet('/api/data').reply(200, { data: 'success' });
const response = await api.get('/api/data');
expect(response.data).toEqual({ data: 'success' });
});
});
总结
无感刷新Token机制是现代Web应用提升用户体验的关键技术,它巧妙地在安全性和用户体验之间找到了平衡点。通过合理的架构设计和细致的实现,我们可以让用户几乎感知不到身份验证的存在,同时保持系统的安全性。
关键要点:
- 双Token系统各司其职:Access Token短期有效,Refresh Token长期有效
- Axios拦截器是实现无感刷新的核心技术
- 并发控制和错误处理是确保稳定性的关键
- 安全措施必须到位:HttpOnly Cookie、SameSite属性等
实现这一机制后,你的应用将提供更加流畅的用户体验,用户再也不会被突然的"登录过期"提示所打扰。