feat(auth): 添加401未授权统一处理机制

- 在API服务层实现401状态码的统一拦截和处理
- 添加防抖机制,避免短时间内重复处理401错误
- 支持应用层注册自定义的未授权处理器
- 在应用启动时注册401处理器,自动清除登录状态并跳转到登录页
- 同时处理普通请求和流式请求的401响应
This commit is contained in:
richarjiang
2025-11-18 15:59:47 +08:00
parent 21e57634e0
commit 9d424c7bd2
2 changed files with 120 additions and 4 deletions

View File

@@ -1,8 +1,87 @@
import { buildApiUrl } from '@/constants/Api';
import AsyncStorage from '@/utils/kvStore';
import { Alert } from 'react-native';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
// 401处理的防抖机制
let is401Handling = false;
let last401Time = 0;
const DEBOUNCE_INTERVAL = 3000; // 3秒内只处理一次401
// 401处理回调函数由应用层注册
type UnauthorizedHandler = () => void | Promise<void>;
let unauthorizedHandler: UnauthorizedHandler | null = null;
/**
* 注册401未授权处理器
* 应该在应用启动时调用,传入退出登录的函数
*/
export function setUnauthorizedHandler(handler: UnauthorizedHandler) {
unauthorizedHandler = handler;
console.log('[API] 401处理器已注册');
}
/**
* 统一处理401未授权错误
* 包含防抖机制,避免多次提示
*/
async function handle401Unauthorized() {
const now = Date.now();
// 防抖如果正在处理或者距离上次处理不到3秒直接返回
if (is401Handling || (now - last401Time < DEBOUNCE_INTERVAL)) {
console.log('[API] 401处理防抖跳过重复处理');
return;
}
is401Handling = true;
last401Time = now;
try {
console.log('[API] 检测到401未授权开始处理登录过期');
// 清除本地token
await AsyncStorage.removeItem(STORAGE_KEYS.authToken);
await setAuthToken(null);
// 提示用户
Alert.alert(
'登录已过期',
'您的登录状态已过期,请重新登录',
[
{
text: '确定',
onPress: () => {
console.log('[API] 用户确认登录过期提示');
// 调用注册的处理器(如果存在)
if (unauthorizedHandler) {
try {
const result = unauthorizedHandler();
if (result instanceof Promise) {
result.catch(err => {
console.error('[API] 401处理器执行失败:', err);
});
}
} catch (err) {
console.error('[API] 401处理器执行失败:', err);
}
}
}
}
],
{ cancelable: false }
);
} catch (error) {
console.error('[API] 处理401错误时发生异常:', error);
} finally {
// 延迟重置处理标志,确保不会立即再次触发
setTimeout(() => {
is401Handling = false;
}, 1000);
}
}
let inMemoryToken: string | null = null;
export async function setAuthToken(token: string | null): Promise<void> {
@@ -52,6 +131,12 @@ async function doFetch<T>(path: string, options: ApiRequestOptions = {}): Promis
console.log('json', json);
if (!response.ok) {
// 检查是否为401未授权
if (response.status === 401) {
console.log('[API] 检测到401状态码触发登录过期处理');
await handle401Unauthorized();
}
const errorMessage = (json && (json.message || json.error)) || `HTTP ${response.status}`;
const error = new Error(errorMessage);
// @ts-expect-error augment
@@ -181,7 +266,15 @@ export async function postTextStream(path: string, body: any, callbacks: TextStr
try { console.log('[AI_CHAT][stream] done', { status: xhr.status }); } catch { }
if (!resolved) {
cleanup();
if (xhr.status >= 200 && xhr.status < 300) {
if (xhr.status === 401) {
// 处理401未授权
console.log('[AI_CHAT][stream] 检测到401状态码触发登录过期处理');
handle401Unauthorized().catch(err => {
console.error('[AI_CHAT][stream] 处理401错误时发生异常:', err);
});
const error = new Error('登录已过期');
try { callbacks.onError?.(error); } catch { }
} else if (xhr.status >= 200 && xhr.status < 300) {
try { callbacks.onEnd?.(conversationIdFromHeader); } catch { }
} else {
const error = new Error(`HTTP ${xhr.status}`);