feat(auth): 添加401未授权统一处理机制
- 在API服务层实现401状态码的统一拦截和处理 - 添加防抖机制,避免短时间内重复处理401错误 - 支持应用层注册自定义的未授权处理器 - 在应用启动时注册401处理器,自动清除登录状态并跳转到登录页 - 同时处理普通请求和流式请求的401响应
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import '@/i18n';
|
import '@/i18n';
|
||||||
import { DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
import { DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
||||||
import { useFonts } from 'expo-font';
|
import { useFonts } from 'expo-font';
|
||||||
import { Stack } from 'expo-router';
|
import { Stack, useRouter } from 'expo-router';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
import 'react-native-reanimated';
|
import 'react-native-reanimated';
|
||||||
@@ -19,7 +19,7 @@ import { WaterRecordSource } from '@/services/waterRecords';
|
|||||||
import { workoutMonitorService } from '@/services/workoutMonitor';
|
import { workoutMonitorService } from '@/services/workoutMonitor';
|
||||||
import { store } from '@/store';
|
import { store } from '@/store';
|
||||||
import { hydrateActiveSchedule, selectActiveFastingSchedule } from '@/store/fastingSlice';
|
import { hydrateActiveSchedule, selectActiveFastingSchedule } from '@/store/fastingSlice';
|
||||||
import { fetchMyProfile, setPrivacyAgreed } from '@/store/userSlice';
|
import { fetchMyProfile, logout, setPrivacyAgreed } from '@/store/userSlice';
|
||||||
import { createWaterRecordAction } from '@/store/waterSlice';
|
import { createWaterRecordAction } from '@/store/waterSlice';
|
||||||
import { loadActiveFastingSchedule } from '@/utils/fasting';
|
import { loadActiveFastingSchedule } from '@/utils/fasting';
|
||||||
import { initializeHealthPermissions } from '@/utils/health';
|
import { initializeHealthPermissions } from '@/utils/health';
|
||||||
@@ -31,7 +31,7 @@ import { DialogProvider } from '@/components/ui/DialogProvider';
|
|||||||
import { MembershipModalProvider } from '@/contexts/MembershipModalContext';
|
import { MembershipModalProvider } from '@/contexts/MembershipModalContext';
|
||||||
import { ToastProvider } from '@/contexts/ToastContext';
|
import { ToastProvider } from '@/contexts/ToastContext';
|
||||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
import { STORAGE_KEYS } from '@/services/api';
|
import { STORAGE_KEYS, setUnauthorizedHandler } from '@/services/api';
|
||||||
import { BackgroundTaskManager } from '@/services/backgroundTaskManagerV2';
|
import { BackgroundTaskManager } from '@/services/backgroundTaskManagerV2';
|
||||||
import { fetchChallenges } from '@/store/challengesSlice';
|
import { fetchChallenges } from '@/store/challengesSlice';
|
||||||
import AsyncStorage from '@/utils/kvStore';
|
import AsyncStorage from '@/utils/kvStore';
|
||||||
@@ -52,6 +52,7 @@ if (__DEV__) {
|
|||||||
|
|
||||||
function Bootstrapper({ children }: { children: React.ReactNode }) {
|
function Bootstrapper({ children }: { children: React.ReactNode }) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const router = useRouter();
|
||||||
const { profile, onboardingCompleted } = useAppSelector((state) => state.user);
|
const { profile, onboardingCompleted } = useAppSelector((state) => state.user);
|
||||||
const activeFastingSchedule = useAppSelector(selectActiveFastingSchedule);
|
const activeFastingSchedule = useAppSelector(selectActiveFastingSchedule);
|
||||||
const [showPrivacyModal, setShowPrivacyModal] = React.useState(false);
|
const [showPrivacyModal, setShowPrivacyModal] = React.useState(false);
|
||||||
@@ -62,6 +63,28 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
// 初始化快捷动作处理
|
// 初始化快捷动作处理
|
||||||
useQuickActions();
|
useQuickActions();
|
||||||
|
|
||||||
|
// 注册401未授权处理器(应用启动时执行一次)
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handle401 = async () => {
|
||||||
|
try {
|
||||||
|
logger.info('[401处理] 开始处理登录过期');
|
||||||
|
|
||||||
|
// 清除Redux状态
|
||||||
|
await dispatch(logout());
|
||||||
|
|
||||||
|
// 跳转到登录页
|
||||||
|
router.push('/auth/login');
|
||||||
|
|
||||||
|
logger.info('[401处理] 登录过期处理完成');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[401处理] 处理失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setUnauthorizedHandler(handle401);
|
||||||
|
logger.info('[401处理器] 已注册到API服务');
|
||||||
|
}, [dispatch, router]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (fastingHydrationRequestedRef.current) return;
|
if (fastingHydrationRequestedRef.current) return;
|
||||||
if (activeFastingSchedule) {
|
if (activeFastingSchedule) {
|
||||||
|
|||||||
@@ -1,8 +1,87 @@
|
|||||||
import { buildApiUrl } from '@/constants/Api';
|
import { buildApiUrl } from '@/constants/Api';
|
||||||
import AsyncStorage from '@/utils/kvStore';
|
import AsyncStorage from '@/utils/kvStore';
|
||||||
|
import { Alert } from 'react-native';
|
||||||
|
|
||||||
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
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;
|
let inMemoryToken: string | null = null;
|
||||||
|
|
||||||
export async function setAuthToken(token: string | null): Promise<void> {
|
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);
|
console.log('json', json);
|
||||||
|
|
||||||
if (!response.ok) {
|
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 errorMessage = (json && (json.message || json.error)) || `HTTP ${response.status}`;
|
||||||
const error = new Error(errorMessage);
|
const error = new Error(errorMessage);
|
||||||
// @ts-expect-error augment
|
// @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 { }
|
try { console.log('[AI_CHAT][stream] done', { status: xhr.status }); } catch { }
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
cleanup();
|
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 { }
|
try { callbacks.onEnd?.(conversationIdFromHeader); } catch { }
|
||||||
} else {
|
} else {
|
||||||
const error = new Error(`HTTP ${xhr.status}`);
|
const error = new Error(`HTTP ${xhr.status}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user