token-refresh-lock
v1.0.2
Published
基于单例刷新锁 + 请求队列的 Axios Token 无感刷新 SDK
Maintainers
Readme
token-refresh-lock
基于单例刷新锁 + 请求队列的 Axios Token 无感刷新 SDK。
当多个并发请求同时收到 401 时,SDK 只发起 一次 刷新请求,其余请求自动排队,刷新成功后批量重试 —— 整个过程用户完全无感知。
适用场景
典型场景:Access Token 短期过期
大多数 Web 系统采用双 Token 机制(Access Token + Refresh Token),Access Token 有效期通常只有 15~30 分钟。当用户正在使用页面时 Token 过期,如果不做无感刷新,用户会突然被踢到登录页,体验极差。本 SDK 自动拦截 401 并刷新 Token,用户完全无感知。
并发请求集中过期
页面加载时可能同时发出 10+ 个 API 请求(用户信息、权限列表、菜单、通知等),如果此时 Access Token 刚好过期,10 个请求全部返回 401。没有锁机制会触发 10 次刷新请求,导致后端拒绝重复刷新或 Refresh Token 被提前消耗。本 SDK 通过单例锁保证只刷新一次,其余请求排队等待。
适用项目类型
| 项目类型 | 是否适用 | 说明 | |----------|:--------:|------| | SPA 单页应用(Vue / React / Angular) | ✅ | 页面不刷新,Token 过期后需前端自行处理 | | SSR / Nuxt / Next.js(客户端请求) | ✅ | 客户端 Axios 实例同样适用 | | Electron / Tauri 桌面应用 | ✅ | 内嵌 Web 页面中的请求拦截 | | 微信小程序(基于 axios 封装) | ✅ | 只要是 Axios 实例即可 | | 纯 SSR 服务端渲染请求 | ❌ | 服务端无 Cookie 场景需自行处理 | | 非 Axios 项目(fetch / uni.request) | ❌ | 当前仅支持 Axios |
适合引入的时机
- 项目登录后使用 JWT Access Token 进行接口鉴权
- Access Token 有效期较短(≤ 30 分钟),需要自动续期
- 页面存在多个并发请求,需避免重复刷新
- 后端已提供
/auth/refresh刷新接口(配合 HttpOnly Cookie) - 需要用户无感知地保持登录状态
不适合的场景
- 后端使用 Session 鉴权,不涉及 Token 过期
- Access Token 有效期极长(如 7 天),过期概率极低
- 项目未使用 Axios 作为 HTTP 客户端
核心机制
| 机制 | 说明 |
|------|------|
| 单例锁 isRefreshing | 确保同一时间只发一次刷新请求,避免并发雪崩 |
| 请求队列 failedQueue | 401 请求自动排队,刷新成功后批量重试 |
| 重试标记 _tokenRefreshRetry | 防止重试请求再次触发刷新,避免无限循环 |
安装
npm install token-refresh-lock前置依赖:项目需已安装
axios >= 0.21.0。
快速上手
import axios from 'axios';
import { createTokenRefreshInterceptor } from 'token-refresh-lock';
const instance = axios.create({ baseURL: '/api' });
createTokenRefreshInterceptor(instance, {
refreshApiCall: () => instance.post('/auth/refresh'),
extractToken: (res) => res.data.accessToken,
onRefreshFailure: () => {
// 清除本地认证状态,跳转登录
localStorage.removeItem('auth');
window.location.href = '/login';
},
});
// 之后正常使用 instance 即可,401 会自动处理
instance.get('/api/user').then(console.log);API
createTokenRefreshInterceptor(instance, options)
创建并挂载 Token 刷新拦截器。
参数:
| 参数 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| instance | AxiosInstance | 是 | Axios 实例 |
| options | TokenRefreshOptions | 是 | 配置项,见下表 |
返回值: InterceptorController — 包含 eject() 方法,可手动卸载拦截器。
TokenRefreshOptions
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|:----:|--------|------|
| refreshApiCall | () => Promise<AxiosResponse> | ✅ | — | 刷新 Token 的 API 请求函数。后端应从 HttpOnly Cookie 中读取 Refresh Token,前端无需手动传递 |
| extractToken | (response: AxiosResponse) => string | ✅ | — | 从刷新接口的响应中提取新的 Access Token |
| setAuthHeader | (config, token) => void | — | 设置 Authorization: Bearer <token> | 自定义如何将 Token 写入请求头 |
| statusCodes | number[] | — | [401] | 触发刷新的 HTTP 状态码列表 |
| maxRetryCount | number | — | 1 | 原始请求最大重试次数 |
| onRefreshSuccess | (token, response) => void | — | — | 刷新成功后的回调,可用于持久化存储新 Token |
| onRefreshFailure | (error) => void | — | — | 刷新失败后的回调,通常用于清除认证状态并跳转登录页 |
InterceptorController
| 方法 | 说明 |
|------|------|
| eject() | 卸载拦截器,清空请求队列。适用于用户主动登出等场景 |
使用示例
基础用法
import axios from 'axios';
import { createTokenRefreshInterceptor } from 'token-refresh-lock';
const instance = axios.create();
createTokenRefreshInterceptor(instance, {
refreshApiCall: () => instance.post('/auth/refresh'),
extractToken: (res) => res.data.accessToken,
});自定义请求头
如果你的项目使用自定义 Header 传递 Token:
createTokenRefreshInterceptor(instance, {
refreshApiCall: () => instance.post('/auth/refresh'),
extractToken: (res) => res.data.accessToken,
setAuthHeader: (config, token) => {
config.headers['X-Access-Token'] = token;
},
});刷新成功后保存 Token
createTokenRefreshInterceptor(instance, {
refreshApiCall: () => instance.post('/auth/refresh'),
extractToken: (res) => res.data.accessToken,
onRefreshSuccess: (token) => {
sessionStorage.setItem('access_token', token);
},
});完整配置
import axios from 'axios';
import { createTokenRefreshInterceptor } from 'token-refresh-lock';
const instance = axios.create({ baseURL: '/api' });
const controller = createTokenRefreshInterceptor(instance, {
// 必填:刷新 Token 的请求
refreshApiCall: () => instance.post('/auth/refresh'),
// 必填:从响应中提取新 Access Token
extractToken: (res) => res.data.accessToken,
// 可选:自定义设置请求头
setAuthHeader: (config, token) => {
config.headers['Authorization'] = `Bearer ${token}`;
},
// 可选:触发刷新的状态码
statusCodes: [401],
// 可选:最大重试次数
maxRetryCount: 1,
// 可选:刷新成功回调
onRefreshSuccess: (token, response) => {
console.log('Token 刷新成功:', token);
sessionStorage.setItem('access_token', token);
},
// 可选:刷新失败回调
onRefreshFailure: (error) => {
console.error('Token 刷新失败:', error);
sessionStorage.removeItem('access_token');
window.location.href = '/login';
},
});
// 用户登出时卸载拦截器
function logout() {
controller.eject();
sessionStorage.removeItem('access_token');
window.location.href = '/login';
}配合 Vue / React 使用
Vue (Pinia):
// stores/auth.ts
import { defineStore } from 'pinia';
import axios from 'axios';
import { createTokenRefreshInterceptor } from 'token-refresh-lock';
const api = axios.create({ baseURL: '/api' });
export const useAuthStore = defineStore('auth', () => {
const token = ref('');
createTokenRefreshInterceptor(api, {
refreshApiCall: () => api.post('/auth/refresh'),
extractToken: (res) => res.data.accessToken,
onRefreshSuccess: (newToken) => { token.value = newToken; },
onRefreshFailure: () => {
token.value = '';
router.push('/login');
},
});
return { token };
});React:
// lib/request.ts
import axios from 'axios';
import { createTokenRefreshInterceptor } from 'token-refresh-lock';
const api = axios.create({ baseURL: '/api' });
createTokenRefreshInterceptor(api, {
refreshApiCall: () => api.post('/auth/refresh'),
extractToken: (res) => res.data.accessToken,
onRefreshSuccess: (token) => {
sessionStorage.setItem('access_token', token);
},
onRefreshFailure: () => {
sessionStorage.removeItem('access_token');
window.location.href = '/login';
},
});
export default api;工作流程
请求 A ──→ 401 ──→ 发起刷新 ──→ 成功 ──→ 携带新 Token 重试 A ──→ 200 ✅
请求 B ──→ 401 ──→ 正在刷新中,加入队列 ──┘
请求 C ──→ 401 ──→ 正在刷新中,加入队列 ──┘
└──→ 拿到新 Token,重试 B、C ──→ 200 ✅后端配合:Refresh Token 存储方式
本 SDK 对后端 Refresh Token 的传递方式没有硬性要求。无论后端是使用 HttpOnly Cookie、请求体传递、还是自定义 Header,都可以通过 refreshApiCall 参数自行控制。
但这里强烈推荐后端将 Refresh Token 存储在 HttpOnly Cookie 中,这是业界公认的安全最佳实践。下面分别说明推荐方案和替代方案。
什么是 HttpOnly Cookie?
后端在登录成功后,通过 Set-Cookie 响应头将 Refresh Token 下发给浏览器:
HTTP/1.1 200 OK
Set-Cookie: refreshToken=abc123; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh设置了 HttpOnly 标记后,JavaScript 无法通过 document.cookie 读取该 Cookie,但浏览器会在后续请求中自动携带。这意味着:
- 前端永远无法获取 Refresh Token 的值
- XSS 攻击即使注入了恶意脚本,也无法窃取 Refresh Token
- 浏览器会在调用
/auth/refresh时自动携带该 Cookie,无需前端手动传递
为什么不用 localStorage?
| 存储方式 | JS 可读 | XSS 可窃取 | 适用场景 | |----------|:-------:|:-----------:|----------| | localStorage | ✅ | ✅ 危险 | 不适合存 Token | | sessionStorage | ✅ | ✅ 危险 | 可存 Access Token(短期) | | HttpOnly Cookie | ❌ | ❌ 安全 | 推荐存 Refresh Token | | JS 内存变量 | ✅ | ✅ | 可存 Access Token(最短命) |
如果将 Refresh Token 存在 localStorage,一旦遭遇 XSS 攻击,攻击者可以长期盗用用户账号(Refresh Token 有效期通常 7 天)。而 HttpOnly Cookie 让前端完全不可读,从根本上杜绝了这个风险。
替代方案:请求体传递 Refresh Token
如果你的后端暂时无法使用 HttpOnly Cookie(如跨域场景、老项目改造等),也可以在 refreshApiCall 中手动传递 Refresh Token:
createTokenRefreshInterceptor(instance, {
// 手动从存储中读取 Refresh Token 并放入请求体
refreshApiCall: () => {
const refreshToken = sessionStorage.getItem('refresh_token');
return instance.post('/auth/refresh', { refreshToken });
},
extractToken: (res) => res.data.accessToken,
});注意:这种方式虽然能正常工作,但 Refresh Token 暴露给了 JavaScript,存在 XSS 窃取风险。仅建议作为过渡方案,长期仍应迁移到 HttpOnly Cookie。
前后端协作流程(HttpOnly Cookie 方案)
1. 用户登录
后端 → Set-Cookie: refreshToken=xxx; HttpOnly (浏览器自动保存)
后端 → 响应体返回 accessToken (前端保存在内存中)
2. 正常请求
前端 → Authorization: Bearer <accessToken> (手动设置)
前端 → Cookie: refreshToken=xxx (浏览器自动携带)
3. Access Token 过期(401)
前端 → 调用 /auth/refresh
浏览器 → 自动携带 HttpOnly Cookie 中的 refreshToken
后端 → 校验 refreshToken,返回新的 accessToken
前端 → 用新 accessToken 重试原请求
4. Refresh Token 也过期
后端 → 返回 401
前端 → onRefreshFailure 回调,跳转登录页后端接口要求
后端只需提供一个刷新接口,本 SDK 通过 refreshApiCall 调用:
POST /auth/refresh
Cookie: refreshToken=xxx (浏览器自动携带,前端无需处理)
→ 成功:{ "accessToken": "new_token_xxx" }
→ 失败:401 Unauthorized(Refresh Token 也过期)注意:刷新接口的路径和响应格式由你通过
refreshApiCall和extractToken参数自定义,SDK 不做任何假设。
Access Token 存储建议
| Token 类型 | 推荐存储方式 | 原因 |
|------------|-------------|------|
| Access Token | 内存变量 / sessionStorage | 短期有效,避免持久化泄露 |
| Refresh Token | HttpOnly Cookie(后端设置) | 前端不可读,防 XSS 窃取 |
切勿将任何 Token 存入
localStorage,这是 XSS 攻击的高价值目标。
构建
npm run build产出 dist/ 目录,同时支持 CJS 和 ESM 格式,并附带 .d.ts 类型声明。
License
MIT
