feat: 前端 401 拦截器 — access token 过期时自动刷新并重试原请求

api.ts:
- 增加 setAuthCallbacks 回调注入机制(避免 api/auth 循环依赖)
- request() 对非公开路径自动附加 Authorization header
- 收到 401 时自动触发 refresh token 刷新,成功后重试原请求
- 并发保护:多个 401 只触发一次 refresh,其余等待同一 Promise
- refreshTokenDirect 内部方法绕过 401 拦截避免递归

auth.tsx:
- 用 ref 保存 persistAuth/scheduleRefresh 最新引用(避免闭包陈旧)
- 初始化时调用 setAuthCallbacks 注入认证回调
This commit is contained in:
hhs
2026-06-20 14:57:27 +08:00
parent ea70d2efc6
commit d78cdb509d
2 changed files with 131 additions and 3 deletions

View File

@@ -15,6 +15,7 @@ import {
} from "react";
import * as api from "./api";
import type { AuthUser } from "./api";
import { setAuthCallbacks } from "./api";
import {
clearAuth,
loadAccessToken,
@@ -108,6 +109,29 @@ export function AuthProvider({ children }: { children: ReactNode }) {
[clearRefreshTimer, persistAuth]
);
// 用 ref 保存最新回调,供 API 层 401 拦截器使用(避免闭包陈旧)
const persistAuthRef = useRef(persistAuth);
const scheduleRefreshRef = useRef(scheduleRefresh);
persistAuthRef.current = persistAuth;
scheduleRefreshRef.current = scheduleRefresh;
// 注册 API 层认证回调(用于 401 拦截器)
useEffect(() => {
setAuthCallbacks({
getAccessToken: () => loadAccessToken(),
getRefreshToken: () => loadRefreshToken(),
onRefreshSuccess: (u, at, rt) => {
persistAuthRef.current(u, at, rt);
scheduleRefreshRef.current(at);
},
onRefreshFailed: () => {
clearAuth();
setUser(null);
setAccessToken(null);
},
});
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// 初始化:检查已有 token 并尝试刷新
useEffect(() => {
const init = async () => {