使用重定向进行 Expo Router 身份验证
编辑
如何使用 Expo Router 实现身份验证并保护路由。
SDK 53 引入了 受保护的路由,这是一种更强大的身份验证处理方法。如果您使用的是 SDK 52 及更早版本,请遵循本指南。

学习如何在您的 Expo Router 项目中实现身份验证流程。
使用 Expo Router,所有路由始终定义并可访问。您可以使用运行时逻辑根据用户是否经过身份验证将其重定向离开特定屏幕。有两种不同的技术用于在路由中对用户进行身份验证。本指南提供了一个示例,演示标准原生应用的功能。
使用 React 上下文和路由组
通常会限制某些路由仅向未通过身份验证的用户开放。这可以通过使用 React 上下文和路由组以有序的方式实现。考虑以下项目结构,其中有一个始终可访问的 /sign-in 路由和一个需要身份验证的 (app) 组:
app_layout.tsxsign-in.tsx始终可访问 (app)_layout.tsx保护子路由index.tsx需要授权 1
要遵循上述示例,设置一个 React 上下文提供者,可以将身份验证会话暴露给整个应用程序。您可以实现自定义身份验证会话提供者或使用下面的 示例身份验证上下文。
示例身份验证上下文
此提供者使用模拟实现。您可以将其替换为自己的 身份验证提供者。
import { useContext, 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 = useContext(AuthContext); if (!value) { throw new Error('useSession 必须被 <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 并在网页本地存储中存储。
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 (process.env.EXPO_OS === 'web') { if (value === null) { localStorage.removeItem(key); } else { localStorage.setItem(key, value); } } 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
在根布局中使用 SessionProvider,将身份验证上下文提供给整个应用程序。必须确保在触发任何导航事件之前挂载 <Slot />。否则将抛出运行时错误。
import { Slot } from 'expo-router'; import { SessionProvider } from '../ctx'; export default function Root() { // 设置身份验证上下文并在其中渲染布局。 return ( <SessionProvider> <Slot /> </SessionProvider> ); }
3
创建一个嵌套的 布局路由,在渲染子路由组件之前检查用户是否经过身份验证。如果用户未通过身份验证,此布局路由将用户重定向到登录屏幕。
import { Text } from 'react-native'; import { Redirect, Stack } from 'expo-router'; import { useSession } from '../../ctx'; export default function AppLayout() { const { session, isLoading } = useSession(); // 您可以保持闪屏开放,或渲染一个加载屏幕,就像我们在这里所做的。 if (isLoading) { return <Text>Loading...</Text>; } // 仅在 (app) 组的布局中需要身份验证,用户 // 需要能够访问 (auth) 组并再次登录。 if (!session) { // 在网页上,静态渲染将停在这里,因为用户未通过身份验证 // 在执行页面渲染的无头 Node 进程中。 return <Redirect href="/sign-in" />; } // 该布局可以延迟,因为它不是根布局。 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
实现一个经过身份验证的屏幕,让用户注销。
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={() => { // `app/(app)/_layout.tsx` 将重定向到登录屏幕。 signOut(); }}> Sign Out </Text> </View> ); }
您现在有一个应用程序,可以在检查初始身份验证状态时呈现加载状态,并在用户未通过身份验证时重定向到登录屏幕。如果用户访问带有身份验证检查的任何路由的深层链接,他们将被重定向到登录屏幕。
替代加载状态
使用 Expo Router,加载初始身份验证状态时必须向屏幕呈现某些内容。在上面的示例中,应用布局呈现了一个加载消息。或者,您可以使 index 路由成为加载状态,并将初始路由移到 /home,这类似于 X 的工作方式。
模态和每路由身份验证
另一个常见模式是在应用程序顶部渲染一个登录模态。这使您能够在身份验证完成时解除并部分保留深层链接。然而,这种模式需要在后台渲染路由,因为这些路由需要在没有身份验证的情况下处理数据加载。
app_layout.tsx声明全局会话上下文(app)_layout.tsxsign-in.tsx在根上呈现模态(root)_layout.tsx保护子路由index.tsx需要授权 import { Stack } from 'expo-router'; export const unstable_settings = { anchor: '(root)', }; export default function AppLayout() { return ( <Stack> <Stack.Screen name="(root)" /> <Stack.Screen name="sign-in" options={{ presentation: 'modal', }} /> </Stack> ); }
无导航导航
当应用程序尝试在 根布局 中没有挂载导航器时执行导航时,您可能会遇到以下错误。
错误:尝试在挂载根布局组件之前进行导航。确保根布局组件在第一次渲染时正在渲染一个 Slot 或其他导航器。
要解决此问题,请添加一个组并将条件逻辑移动到下一级。
之前
app_layout.tsxabout.tsxexport default function RootLayout() { React.useEffect(() => { // 此导航事件将触发上述错误。 router.push('/about'); }, []); // 此条件语句创建问题,因为根布局的 // 内容(Slot)必须在发生任何导航事件之前挂载。 if (isLoading) { return <Text>Loading...</Text>; } return <Slot />; }
之后
app_layout.tsx(app)_layout.tsx将条件逻辑移到下一级about.tsxexport default function RootLayout() { return <Slot />; }
export default function RootLayout() { React.useEffect(() => { router.push('/about'); }, []); // 延迟呈现此嵌套布局的内容是可以的。我们无法 // 延迟呈现根布局的内容,因为导航事件(重定向) // 会在根布局的内容被挂载之前触发。 if (isLoading) { return <Text>Loading...</Text>; } return <Slot />; }
中间件
传统上,网站可能会利用某种形式的服务器端重定向来保护路由。Expo Router 在网页上目前仅支持构建时静态生成,并不支持自定义中间件或服务。这可以在未来添加,以提供更优化的网页体验。在此期间,可以通过使用客户端重定向和加载状态来实现身份验证。