跳到主要内容

认证与刷新 Token

请求层一旦进入登录态系统,通常都会碰到这几个问题:

  • token 放哪里
  • 什么时候带上
  • 过期后怎么刷新
  • 多个请求同时 401 时怎么避免重复刷新
  • 刷新失败后怎么统一退出登录

这篇主要收这条链路。

一条最常见的结构

src/
request/
client.ts
auth.ts
refresh.ts
api/
auth.ts
user.ts

大致可以这么分:

  • client.ts:请求实例
  • auth.ts:token 的读写
  • refresh.ts:刷新逻辑和并发控制

token 一般放哪

最常见的几种方案:

  • 内存
  • localStorage
  • sessionStorage
  • cookie

更稳的判断不是“哪种最流行”,而是当前项目的安全边界和服务端配合方式。

一个常见的 axios 写法

import axios from 'axios'

const http = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
})

let refreshPromise: Promise<string | null> | null = null

function getAccessToken() {
return localStorage.getItem('access_token')
}

function setAccessToken(token: string) {
localStorage.setItem('access_token', token)
}

async function refreshAccessToken() {
const res = await axios.post('/auth/refresh', {
refreshToken: localStorage.getItem('refresh_token'),
})

const token = res.data.accessToken
setAccessToken(token)
return token
}

http.interceptors.request.use((config) => {
const token = getAccessToken()

if (token) {
config.headers.Authorization = `Bearer ${token}`
}

return config
})

http.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config

if (error.response?.status !== 401 || originalRequest._retry) {
return Promise.reject(error)
}

originalRequest._retry = true

if (!refreshPromise) {
refreshPromise = refreshAccessToken().finally(() => {
refreshPromise = null
})
}

const newToken = await refreshPromise

if (!newToken) {
return Promise.reject(error)
}

originalRequest.headers.Authorization = `Bearer ${newToken}`
return http(originalRequest)
}
)

这里最关键的点

1. 不要重复刷新

如果 5 个请求同时 401,而每个都自己去刷新 token,后面通常会乱。

所以更稳的方式是:

  • 刷新过程只保留一个 refreshPromise
  • 后续 401 请求等它完成
  • 刷新成功后统一重放

2. 要有 _retry 标记

不然一旦刷新后的请求还是 401,很容易进入死循环。

3. 刷新失败要统一收口

例如:

  • 清空本地登录态
  • 跳转登录页
  • 清理用户缓存

和 TanStack Query 怎么配

TanStack Query 不应该自己处理 token 刷新细节。更稳的结构是:

  • 请求层负责 401、刷新和重放
  • Query 层只消费请求结果

这样页面和 query hooks 不会被鉴权逻辑污染。

收尾时更该多看两眼的地方

认证链路里更容易出问题的,通常不是“header 带没带上”,而是:

  • 并发 401 怎么收口
  • 刷新失败怎么退场
  • 重放请求是否安全
  • 页面缓存和用户态怎么一起清

这些边界如果一开始就收好,后面会轻松很多。