使用重定向在 Expo Router 中进行身份验证
编辑页面
如何使用 Expo Router 实现身份验证并保护路由。
For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.
SDK 53 引入了受保护路由,这是一种更强大的身份验证处理方式。如果您使用的是 SDK 52 或更早版本,请遵循本指南。

了解如何在你的 Expo Router 项目中实现认证流程。
使用 Expo Router 时,所有路由都会始终被定义并可访问。你可以使用运行时逻辑,根据用户是否已认证将其重定向离开特定屏幕。路由内用户认证有两种不同的技术。本指南提供了一个示例,展示标准原生应用的功能。
使用 React Context 和路由组
通常会限制特定路由,仅允许未认证用户访问。通过使用 React Context 和路由组,可以以一种有组织的方式实现这一点。请考虑下面的项目结构,其中有一个始终可访问的 /sign-in 路由,以及一个需要认证的 (app) 组:
app_layout.tsxsign-in.tsx始终可访问 (app)_layout.tsx保护子路由index.tsx需要授权 1
要实现上述示例,请设置一个 React Context 提供者,以便向整个应用暴露认证会话。你可以实现你自己的自定义认证会话提供者,或者使用下面 示例认证上下文 中的实现。
示例认证上下文
这个提供者使用的是一个模拟实现。你可以将其替换为你自己的 认证提供者。
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 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 安全地持久化令牌,并在 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 (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('本地存储不可用:', 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) { // 在 Web 上,由于用户在渲染这些页面的无头 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('/'); }}> 登录 </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(); }}> 登出 </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> ); }
无导航时的导航
当应用尝试在 根布局 中未挂载导航器的情况下执行导航时,你可能会遇到以下错误。
Error: Attempted to navigate before mounting the Root Layout component. Ensure the Root Layout component is rendering a Slot, or other navigator on the first render.
要修复此问题,请添加一个组,并将条件逻辑下移一层。
之前
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 在 Web 上目前仅支持构建时静态生成,并且不支持自定义中间件或服务端提供内容。未来可以添加这些功能,以提供更优化的 Web 体验。与此同时,可以通过使用客户端重定向和加载状态来实现身份验证。