npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

token-refresh-lock

v1.0.2

Published

基于单例刷新锁 + 请求队列的 Axios Token 无感刷新 SDK

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 也过期)

注意:刷新接口的路径和响应格式由你通过 refreshApiCallextractToken 参数自定义,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