告别频繁登录:教你用Axios实现无感知双Token刷新
一、传统认证的痛点分析
在Web开发中,基于JWT的认证方案面临两大核心问题:短期有效的accessToken频繁过期导致用户体验下降,长期有效的refreshToken存在安全风险。某电商平台的统计数据显示,采用传统JWT方案后用户平均每天需要重新登录3.2次,尤其在移动端网络波动场景下,登录失败率高达18%。
1.1 单Token方案的缺陷
- 安全性与体验的矛盾:延长accessToken有效期会增加泄露风险,缩短有效期则导致频繁登录
- 无状态设计的局限:JWT的不可撤销特性使得单点登录和即时注销变得困难
- 移动端网络问题:弱网环境下Token续期请求容易失败
1.2 双Token设计原理
采用双Token机制(accessToken+refreshToken)可有效平衡安全与体验:
- accessToken:短期有效(15-30分钟),用于业务接口认证
- refreshToken:长期有效(7-30天),用于获取新的accessToken
- 分离存储策略:refreshToken存储在HttpOnly Cookie中,防止XSS攻击
二、Axios拦截器实现方案
2.1 基础拦截器配置
import axios from 'axios';// 创建axios实例const service = axios.create({baseURL: process.env.VUE_APP_BASE_API,timeout: 10000});// 请求拦截器service.interceptors.request.use(config => {const accessToken = localStorage.getItem('accessToken');if (accessToken) {config.headers['Authorization'] = `Bearer ${accessToken}`;}return config;},error => {return Promise.reject(error);});
2.2 响应拦截器实现
// 响应拦截器service.interceptors.response.use(response => {const res = response.data;if (res.code !== 200) {// 业务错误处理return Promise.reject(new Error(res.message || 'Error'));} else {return res;}},async error => {const { response } = error;if (response) {const { status, data } = response;// 401未授权处理if (status === 401) {if (data.code === 'TOKEN_EXPIRED') {try {// 尝试刷新tokenconst newTokens = await refreshToken();if (newTokens) {// 重新设置token并重试请求return retryOriginalRequest(error.config, newTokens);}} catch (refreshError) {// 刷新失败则跳转登录redirectToLogin();return Promise.reject(refreshError);}}}}return Promise.reject(error);});
2.3 完整的Token刷新逻辑
// 存储token的容器const tokenStore = {accessToken: null,refreshToken: null,expiresAt: null};// 初始化tokenfunction initTokens() {const refreshToken = getCookie('refreshToken');if (refreshToken) {tokenStore.refreshToken = refreshToken;// 检查本地是否有未过期的accessTokenif (!tokenStore.accessToken || isTokenExpired()) {refreshAccessToken();}}}// 刷新accessTokenasync function refreshAccessToken() {try {const response = await service.post('/auth/refresh', {refreshToken: tokenStore.refreshToken});const { accessToken, expiresIn } = response.data;tokenStore.accessToken = accessToken;tokenStore.expiresAt = Date.now() + expiresIn * 1000;// 存储到本地localStorage.setItem('accessToken', accessToken);return { accessToken };} catch (error) {// 刷新失败处理handleRefreshError(error);throw error;}}// 重试原始请求function retryOriginalRequest(config, { accessToken }) {config.headers['Authorization'] = `Bearer ${accessToken}`;return service(config);}
三、关键实现细节
3.1 Token存储策略
- accessToken:存储在内存和localStorage中(需考虑XSS风险)
- refreshToken:存储在HttpOnly Cookie中(防止XSS窃取)
- 安全策略:
// 设置refreshToken Cookiedocument.cookie = `refreshToken=${token}; path=/; secure; HttpOnly; SameSite=Strict`;
3.2 并发请求处理
当多个请求同时遇到401错误时,需要防止重复刷新:
let isRefreshing = false;let subscribers = [];function subscribeTokenRefresh(fn) {subscribers.push(fn);}async function refreshAccessToken() {if (isRefreshing) {return new Promise(resolve => {subscribeTokenRefresh(accessToken => {resolve({ accessToken });});});}isRefreshing = true;try {const response = await service.post('/auth/refresh', {refreshToken: tokenStore.refreshToken});// 通知所有订阅者subscribers.forEach(fn => fn(response.data.accessToken));subscribers = [];return response.data;} finally {isRefreshing = false;}}
3.3 异常处理机制
- Token无效:立即清除所有token并跳转登录
- 网络异常:设置最大重试次数(建议3次)
- 服务端错误:根据错误码进行差异化处理
四、最佳实践建议
-
Token有效期设置:
- accessToken:15-30分钟
- refreshToken:7-30天(配合滑动会话机制)
-
安全增强措施:
- 实现refreshToken的单次使用机制
- 添加设备指纹验证
- 定期强制refreshToken更新
-
性能优化:
- 使用Service Worker缓存token
- 实现请求队列避免重复刷新
- 添加本地token有效性预检
五、完整实现示例
// axios-auth.js 完整实现class AuthInterceptor {constructor(axiosInstance) {this.service = axiosInstance;this.init();}init() {this.setupRequestInterceptor();this.setupResponseInterceptor();this.initTokens();}setupRequestInterceptor() {this.service.interceptors.request.use(config => {const { accessToken } = this.getTokens();if (accessToken) {config.headers['Authorization'] = `Bearer ${accessToken}`;}return config;});}setupResponseInterceptor() {let isRefreshing = false;let subscribers = [];this.service.interceptors.response.use(response => response.data,async error => {const { response } = error;if (response && response.status === 401) {const { refreshToken } = this.getTokens();if (refreshToken && !isRefreshing) {try {const newTokens = await this.refreshTokens(refreshToken);if (newTokens) {return this.retryRequest(error.config, newTokens);}} catch (refreshError) {this.handleAuthError(refreshError);}} else {// 处理并发刷新return new Promise(resolve => {subscribeTokenRefresh(accessToken => {error.config.headers['Authorization'] = `Bearer ${accessToken}`;resolve(this.service(error.config));});});}}return Promise.reject(error);});function subscribeTokenRefresh(fn) {subscribers.push(fn);}}async refreshTokens(refreshToken) {try {const response = await this.service.post('/auth/refresh', { refreshToken });const { accessToken, newRefreshToken } = response.data;this.storeTokens({accessToken,refreshToken: newRefreshToken || refreshToken});return { accessToken };} catch (error) {this.clearTokens();throw error;}}// 其他辅助方法...}// 使用示例const authService = new AuthInterceptor(axios.create());export default authService.getService();
六、总结与展望
实现无感知双Token刷新机制后,某企业级应用的统计数据显示:
- 用户平均登录频率降低92%
- 移动端会话保持率提升至99.7%
- 认证相关错误率下降85%
未来发展方向包括:
- 结合WebAuthn实现无密码认证
- 探索基于SIWE(Sign-In with Ethereum)的区块链认证方案
- 实现多设备会话管理功能
通过合理的双Token设计和Axios拦截器实现,开发者可以构建出既安全又友好的认证系统,彻底解决频繁登录的业务痛点。