认证与刷新 Token
请求层一旦进入登录态系统,通常都会碰到这几个问题:
- token 放哪里
- 什么时候带上
- 过期后怎么刷新
- 多个请求同时 401 时怎么避免重复刷新
- 刷新失败后怎么统一退出登录
这篇主要收这条链路。
一条最常见的结构
src/
request/
client.ts
auth.ts
refresh.ts
api/
auth.ts
user.ts
大致可以这么分:
client.ts:请求实例auth.ts:token 的读写refresh.ts:刷新逻辑和并发控制
token 一般放哪
最常见的几种方案:
- 内存
localStoragesessionStorage- 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 怎么收口
- 刷新失败怎么退场
- 重放请求是否安全
- 页面缓存和用户态怎么一起清
这些边界如果一开始就收好,后面会轻松很多。