深入理解 Token 认证与无感刷新机制
什么是双 Token 机制?🤔
在现代 Web 应用中,Token 认证机制扮演着至关重要的角色。但为什么我们需要使用双 Token 机制呢?
Access Token 和 Refresh Token 的区别
Access Token 🎫
- 用于访问受保护的资源
- 有效期较短(通常几小时或更短)
- 频繁使用
- 安全要求高
Refresh Token 🔄
- 用于获取新的 Access Token
- 有效期较长(通常几天到几周)
- 使用频率低
- 只用于刷新,不能直接访问资源
认证流程 📝
- 用户登录 → 服务器返回 Access Token 和 Refresh Token
- 使用 Access Token 请求资源
- Access Token 过期 → 服务器返回过期错误
- 使用 Refresh Token 获取新的 Access Token
- 使用新的 Access Token 继续访问资源
无感刷新实现方案 ⚙️
方案一:请求前拦截
javascript
// 请求前判断 token 是否过期
if (isTokenExpired()) {
await refreshToken();
}
// 继续发起原请求优点 ✅
- 可以减少无效请求
- 提高性能和用户体验
缺点 ❌
- 需要后端提供 token 过期时间
- 依赖客户端时间,可能存在安全隐患
方案二:响应后拦截
javascript
// 响应拦截器中处理
if (response.code === TOKEN_EXPIRED) {
await refreshToken();
return retry(originalRequest);
}优点 ✅
- 实现简单
- 不依赖客户端时间
- 更可靠
缺点 ❌
- 会产生额外的请求开销
最佳实践示例 💡
1. 配置常量
javascript
// constants.js
export const TOKEN_CONFIG = {
ACCESS_TOKEN: 'access_token',
REFRESH_TOKEN: 'refresh_token',
TOKEN_HEADER: 'Authorization',
REFRESH_HEADER: 'X-Refresh-Token'
};2. 请求拦截器实现
javascript
// axios 配置
const service = axios.create({
timeout: 30000,
baseURL: process.env.API_BASE_URL
});
// 请求队列管理
let refreshing = false;
const waitQueue = [];
service.interceptors.response.use(
response => response,
async error => {
const { config, response } = error;
if (response?.status === 401 && !config._retry) {
if (refreshing) {
// 将请求加入等待队列
return new Promise(resolve => {
waitQueue.push(() => resolve(service(config)));
});
}
refreshing = true;
config._retry = true;
try {
const newToken = await refreshAccessToken();
// 更新 token 后重试所有等待的请求
waitQueue.forEach(cb => cb());
waitQueue.length = 0;
return service(config);
} catch (err) {
// 刷新失败,跳转登录
router.push('/login');
return Promise.reject(err);
} finally {
refreshing = false;
}
}
return Promise.reject(error);
}
);注意事项 ⚠️
安全性考虑
- Refresh Token 应妥善保存
- 建议使用 HttpOnly Cookie 存储
- 考虑使用 HTTPS 传输
并发请求处理
- 多个请求同时失效时要避免重复刷新
- 使用请求队列管理并发
异常处理
- 刷新失败时要有合适的降级策略
- 考虑网络异常、服务器异常等场景
用户体验
- 刷新过程对用户应该是无感知的
- 可以添加适当的加载提示