Expo Router 的认证
编辑
如何在 Expo Router 中实现认证并保护路由。
注意: 本指南需要 SDK 53 及更高版本。有关该指南的先前版本,请参见 身份验证(重定向)。
使用 Expo Router,所有路由始终被定义并可以访问。您可以使用运行时逻辑根据用户是否已认证将用户重定向到特定屏幕以外的地方。在路由内对用户进行认证有两种不同的技术。 本指南提供了一个示例,演示标准原生应用程序的功能。
使用受保护路由
受保护路由 允许您通过客户端导航来防止用户访问某些路由。如果用户尝试导航到受保护的屏幕,或者如果屏幕在其活动时变为受保护的,他们将被重定向到锚路由(通常是索引屏幕)或堆栈中第一个可用屏幕。 考虑以下项目结构,其中有一个始终可访问的 /sign-in 路由和一个需要认证的 (app) 组:
app_layout.tsx控制哪些内容受到保护sign-in.tsx始终可访问 (app)_layout.tsx需要授权 index.tsx应该受 (app)/_layout 保护1
要遵循上述示例,设置一个 React Context 提供者,它可以将认证会话暴露给整个应用程序。你可以实现你自己的自定义认证会话提供者,或者使用下面的 示例认证上下文。
示例认证上下文
此提供者使用模拟实现。您可以用您自己的 认证提供者 替换它。
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, }); // 使用此钩子访问用户信息。 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> ); }
以下代码片段是一个基本钩子,它通过 expo-secure-store 在本地安全地持久化令牌,并在网络上存储在本地存储中。
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('Local storage is unavailable:', 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('Local storage is unavailable:', 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() { // 设置认证上下文并在其中渲染布局。 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('/'); }}> Sign In </Text> </View> ); }
5
现在修改 RootNavigator 以根据您的 SessionProvider 保护路由。
// All import statements remain the same except you need to import `useSession` from your `ctx.tsx` file. import { SessionProvider, useSession } from '../ctx'; // 以上所有代码保持不变。根据您的 `SessionProvider` 更新 `RootNavigator` 以保护路由如下。 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` 中的守卫会重定向回登录屏幕。 signOut(); }}> Sign Out </Text> </View> ); }
7
创建 app/(app)/_layout.tsx:
import { Stack } from 'expo-router'; export default function AppLayout() { // 这将为所有经过认证的应用路由渲染导航堆栈。 return <Stack />; }
现在您有了一个应用程序,该应用将在初始认证状态加载完成之前显示启动屏幕,并且如果用户未经过认证,将重定向到登录屏幕。如果用户访问任何具有认证检查的路由的深链接,他们将被重定向到登录屏幕。
模态和每路由认证
另一个常见模式是渲染在应用上方的登录模态。这使您能够在认证完成后取消并部分保留深链接。然而,此模式要求在后台渲染路由,因为这些路由需要在没有认证的情况下处理数据加载。
app_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 在网络上当前仅支持构建时静态生成,且不支持自定义中间件或服务。这将在将来添加,以提供更优化的网络体验。在此期间,可以通过使用客户端重定向和加载状态来实现认证。