Expo Router 中的身份验证
编辑页面
如何使用 Expo Router 实现身份验证并保护路由。
For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.
关于本指南的先前版本(SDK 52 及更早版本),请参见 身份验证(重定向)。
使用 Expo Router 时,所有路由始终都会被定义且可访问。你可以使用运行时逻辑,根据用户是否已通过身份验证,将用户重定向到特定屏幕之外。本指南提供了一个示例,演示标准原生应用的功能。
使用受保护路由
受保护路由 允许你通过客户端导航阻止用户访问某些路由。如果用户尝试导航到受保护屏幕,或者某个屏幕在处于激活状态时变为受保护状态,他们会被重定向到锚点路由(通常是 index 屏幕)或栈中第一个可用的屏幕。请考虑以下项目结构,其中有一个始终可访问的 /sign-in 路由,以及一个需要身份验证的 (app) 组:
srcapp_layout.tsx控制哪些内容受保护sign-in.tsx始终可访问 (app)_layout.tsx需要授权 index.tsx应该由 (app)/_layout 保护1
要实现上面的示例,请设置一个 React Context provider,它可以向整个应用暴露身份验证会话。你可以实现自己的自定义身份验证会话提供器,或者使用下面“示例身份验证上下文”中的提供器。
示例身份验证上下文
这个提供器使用的是模拟实现。你可以将其替换为你自己的 身份验证提供器。
import { use, createContext, type PropsWithChildren } from 'react'; import { useStorageState } from './useStorageState'; const AuthContext = createContext<{ signIn: () => void; signOut: () => void; session?: string | null; isLoading: boolean; }>({ signIn: () => null, signOut: () => null, session: null, isLoading: false, }); // 使用此 hook 访问用户信息。 export function useSession() { const value = use(AuthContext); if (!value) { throw new Error('useSession must be wrapped in a <SessionProvider />'); } return value; } export function SessionProvider({ children }: PropsWithChildren) { const [[isLoading, session], setSession] = useStorageState('session'); return ( <AuthContext.Provider value={{ signIn: () => { // 在这里执行登录逻辑 setSession('xxx'); }, signOut: () => { setSession(null); }, session, isLoading, }}> {children} </AuthContext.Provider> ); }
下面的代码片段是一个基础 hook,它会在原生端使用 expo-secure-store 安全地持久化 token,并在 web 端将其存储在本地存储中。
import { useEffect, useCallback, useReducer } from 'react'; import * as SecureStore from 'expo-secure-store'; import { Platform } from 'react-native'; type UseStateHook<T> = [[boolean, T | null], (value: T | null) => void]; function useAsyncState<T>( initialValue: [boolean, T | null] = [true, null], ): UseStateHook<T> { return useReducer( (state: [boolean, T | null], action: T | null = null): [boolean, T | null] => [false, action], initialValue ) as UseStateHook<T>; } export async function setStorageItemAsync(key: string, value: string | null) { if (Platform.OS === 'web') { try { if (value === null) { localStorage.removeItem(key); } else { localStorage.setItem(key, value); } } catch (e) { console.error('本地存储不可用:', e); } } else { if (value == null) { await SecureStore.deleteItemAsync(key); } else { await SecureStore.setItemAsync(key, value); } } } export function useStorageState(key: string): UseStateHook<string> { // 公共 const [state, setState] = useAsyncState<string>(); // 获取 useEffect(() => { if (Platform.OS === 'web') { try { if (typeof localStorage !== 'undefined') { setState(localStorage.getItem(key)); } } catch (e) { console.error('本地存储不可用:', e); } } else { SecureStore.getItemAsync(key).then((value: string | null) => { setState(value); }); } }, [key]); // 设置 const setValue = useCallback( (value: string | null) => { setState(value); setStorageItemAsync(key, value); }, [key] ); return [state, setValue]; }
2
创建一个 SplashScreenController 来管理启动画面。身份验证加载是异步的,因此在身份验证加载完成之前保持启动画面可见。
import { SplashScreen } from 'expo-router'; import { useSession } from './ctx'; SplashScreen.preventAutoHideAsync(); export function SplashScreenController() { const { isLoading } = useSession(); if (!isLoading) { SplashScreen.hide(); } return null; }
3
将 SessionProvider 添加到你的根布局中。这会让整个应用都能访问身份验证上下文。确保 SplashScreenController 位于 SessionProvider 内部。
import { Stack } from 'expo-router'; import { SessionProvider } from '@/ctx'; import { SplashScreenController } from '@/splash'; export default function Root() { // 设置 auth 上下文,并在其中渲染你的布局。 return ( <SessionProvider> <SplashScreenController /> <RootNavigator /> </SessionProvider> ); } // 创建一个新组件,以便后续可以访问 SessionProvider 上下文。 function RootNavigator() { return <Stack />; }
4
创建 /sign-in 屏幕。该屏幕使用 signIn() 切换身份验证。由于此屏幕位于 (app) 组之外,因此渲染此屏幕时不会运行该组的布局和身份验证检查。这使得未登录用户可以访问此屏幕。
import { router } from 'expo-router'; import { Text, View } from 'react-native'; import { useSession } from '@/ctx'; export default function SignIn() { const { signIn } = useSession(); return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text onPress={() => { signIn(); // 登录后进行导航。你可能需要调整这里,以确保在导航前登录成功。 router.replace('/'); }}> 登录 </Text> </View> ); }
5
现在根据你的 SessionProvider 修改 RootNavigator 来保护路由。
// 除了你需要从 `ctx.tsx` 文件中导入 `useSession` 之外,其余导入语句保持不变。 import { SessionProvider, useSession } from '@/ctx'; // 上面的代码都保持不变。下面更新 `RootNavigator`,使其根据你的 `SessionProvider` 保护路由。 function RootNavigator() { const { session } = useSession(); return ( <Stack> <Stack.Protected guard={!!session}> <Stack.Screen name="(app)" /> </Stack.Protected> <Stack.Protected guard={!session}> <Stack.Screen name="sign-in" /> </Stack.Protected> </Stack> ); }
6
实现一个已认证屏幕,让用户可以退出登录。
import { Text, View } from 'react-native'; import { useSession } from '@/ctx'; export default function Index() { const { signOut } = useSession(); return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text onPress={() => { // `RootNavigator` 中的 guard 会重定向回登录屏幕。 signOut(); }}> 退出登录 </Text> </View> ); }
7
创建 src/app/(app)/_layout.tsx:
import { Stack } from 'expo-router'; export default function AppLayout() { // 这会渲染所有已认证应用路由的导航栈。 return <Stack />; }
现在你已经拥有一个应用:它会在初始身份验证状态加载完成之前显示启动画面;如果用户未通过身份验证,则会重定向到登录屏幕。如果用户访问任何带有身份验证检查的深层链接,他们会被重定向到登录屏幕。
模态窗口和按路由认证
另一种常见模式是在应用顶部渲染一个登录模态窗口。这样在认证完成后,你可以关闭模态并部分保留深层链接。然而,这种模式要求路由在后台仍然被渲染,因为这些路由需要在没有身份验证的情况下处理数据加载。
srcapp_layout.tsx声明全局会话上下文(app)_layout.tsxsign-in.tsx在根层之上呈现的模态窗口(root)_layout.tsx保护子路由index.tsx需要授权 import { Stack } from 'expo-router'; export const unstable_settings = { initialRouteName: '(root)', }; export default function AppLayout() { return ( <Stack> <Stack.Screen name="(root)" /> <Stack.Screen name="sign-in" options={{ presentation: 'modal', }} /> </Stack> ); }
更多信息
如需了解更多信息,请阅读 受保护路由文档,以了解更多模式。

了解如何在 Expo Router 版本 5 及更高版本中使用受保护路由来创建身份验证流程。
中间件
传统上,网站可能会利用某种形式的服务器端重定向来保护路由。Expo Router 在 Web 上目前仅支持构建时静态生成,不支持自定义中间件或服务。未来可以添加此功能,以提供更优化的 Web 体验。与此同时,可以通过使用客户端重定向和加载状态来实现身份验证。