Expo 路由中的身份验证

如何使用 Expo Router 实现身份验证和保护路由。


信息 注意: 本指南需要 SDK 53 及以上版本。有关本指南的早期版本,请参阅 身份验证(重定向)

使用 Expo Router,所有路由始终被定义并可访问。你可以使用运行时逻辑根据用户是否已认证,将其重定向到特定页面之外。路由中有两种不同的用户认证技术。本指南提供了一个示例,展示了标准原生应用的功能。

🌐 With Expo Router, all routes are always defined and accessible. You can use runtime logic to redirect users away from specific screens depending on whether they are authenticated. There are two different techniques for authenticating users within routes. This guide provides an example that demonstrates the functionality of standard native apps.

使用受保护的路由

🌐 Using Protected Routes

受保护的路由 允许你防止用户通过客户端导航访问某些路由。如果用户尝试导航到受保护的屏幕,或者屏幕在激活时变为受保护状态,他们将被重定向到锚点路由(通常是索引屏幕)或堆栈中第一个可用的屏幕。考虑以下项目结构,其中有一个始终可访问的 /sign-in 路由和一个需要身份验证的 (app) 组:

app
_layout.tsxControls what is protected
sign-in.tsxAlways accessible
(app)
  _layout.tsxRequires authorization
  index.tsxShould be protected by the (app)/_layout

1

按照上面的例子,设置一个 React Context 提供者,可以将身份验证会话暴露给整个应用。你可以实现自定义的身份验证会话提供者,也可以使用下面 示例身份验证上下文 中的提供者。

🌐 To follow the above example, set up a React Context provider that can expose an authentication session to the entire app. You can implement your custom authentication session provider or use the one from the Example authentication context below.

示例认证上下文

该提供者使用了模拟实现。你可以用自己的身份验证提供者替换它。

🌐 This provider uses a mock implementation. You can replace it with your own authentication provider.

ctx.tsx
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, }); // Use this hook to access the user info. 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: () => { // Perform sign-in logic here setSession('xxx'); }, signOut: () => { setSession(null); }, session, isLoading, }}> {children} </AuthContext.Provider> ); }

以下代码片段是一个基本的 hook,可以在原生环境中使用 expo-secure-store 安全地持久化令牌,并在 Web 上存储到本地存储。

🌐 The following code snippet is a basic hook that persists tokens securely on native with expo-secure-store and in local storage on web.

useStorageState.ts
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> { // Public const [state, setState] = useAsyncState<string>(); // Get 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]); // Set const setValue = useCallback( (value: string | null) => { setState(value); setStorageItemAsync(key, value); }, [key] ); return [state, setValue]; }

2

创建一个 SplashScreenController 来管理启动屏幕。身份验证加载是异步的,因此在身份验证完成之前保持启动屏幕可见。

🌐 Create a SplashScreenController to manage the splash screen. Authentication loading is asynchronous, so keep the splash screen visible until authentication loads.

splash.tsx
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 内部。

🌐 Add the SessionProvider to your root layout. This gives your entire app access to the authentication context. Ensure the SplashScreenController is inside the SessionProvider.

app/_layout.tsx
import { Stack } from 'expo-router'; import { SessionProvider } from '../ctx'; import { SplashScreenController } from '../splash'; export default function Root() { // Set up the auth context and render your layout inside of it. return ( <SessionProvider> <SplashScreenController /> <RootNavigator /> </SessionProvider> ); } // Create a new component that can access the SessionProvider context later. function RootNavigator() { return <Stack />; }

4

创建 /sign-in 屏幕。此屏幕使用 signIn() 切换身份验证。由于此屏幕位于 (app) 组之外,渲染此屏幕时不会运行该组的布局和身份验证检查。这允许未登录的用户访问此屏幕。

🌐 Create the /sign-in screen. This screen toggles authentication using signIn(). Since this screen is outside the (app) group, the group's layout and authentication check do not run when rendering this screen. This lets logged-out users access this screen.

app/sign-in.tsx
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(); // Navigate after signing in. You may want to tweak this to ensure sign-in is successful before navigating. router.replace('/'); }}> Sign In </Text> </View> ); }

5

现在修改 RootNavigator,根据你的 SessionProvider 来保护路由。

🌐 Now modify the RootNavigator to protect routes based on your SessionProvider.

app/_layout.tsx
// All import statements remain the same except you need to import `useSession` from your `ctx.tsx` file. import { SessionProvider, useSession } from '../ctx'; // All of the above code remains unchanged. Update the `RootNavigator` to protect routes based on your `SessionProvider` below. 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

实现一个经过身份验证的界面,让用户可以注销。

🌐 Implement an authenticated screen that lets users sign out.

app/(app)/index.tsx
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={() => { // The guard in `RootNavigator` redirects back to the sign-in screen. signOut(); }}> Sign Out </Text> </View> ); }

7

创建 app/(app)/_layout.tsx:

🌐 Create the app/(app)/_layout.tsx:

app/(app)/_layout.tsx
import { Stack } from 'expo-router'; export default function AppLayout() { // This renders the navigation stack for all authenticated app routes. return <Stack />; }

你现在有一个应用,它将在初始身份验证状态加载完成之前显示启动屏幕,如果用户未经过身份验证,则会重定向到登录屏幕。如果用户访问任何需要身份验证检查的深层链接,他们将被重定向到登录屏幕。

🌐 You now have an app that will present the splash screen until the initial authentication state has loaded and will redirects to the sign-in screen if the user is not authenticated. If a user visits a deep link to any routes with the authentication check, they'll be redirected to the sign-in screen.

模态和每条路由的身份验证

🌐 Modals and per-route authentication

另一种常见的模式是在应用顶部显示一个登录模态窗口。这使你能够在身份验证完成时关闭窗口并部分保留深层链接。然而,这种模式需要在后台渲染路由,因为这些路由需要在没有身份验证的情况下处理数据加载。

🌐 Another common pattern is to render a sign-in modal over the top of the app. This enables you to dismiss and partially preserve deep links when the authentication is complete. However, this pattern requires routes to be rendered in the background as these routes require handling data loading without authentication.

app
_layout.tsxDeclares global session context
(app)
  _layout.tsx
  sign-in.tsxModal presented over the root
  (root)
   _layout.tsxProtects child routes
   index.tsxRequires authorization
app/(app)/_layout.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> ); }

更多信息

🌐 More information

欲了解更多信息,请阅读受保护路由文档以了解更多模式内容。

🌐 For more information, read the Protected routes documentation to learn more about patterns.

如何在 Expo Router 5 及更高版本中使用受保护路由实现顺畅的身份验证
如何在 Expo Router 5 及更高版本中使用受保护路由实现顺畅的身份验证

了解如何在 Expo Router 版本 5 及更高版本中使用受保护路由来创建身份验证流程。

中间件

🌐 Middleware

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

🌐 Traditionally, websites may leverage some form of server-side redirection to protect routes. Expo Router on the web currently only supports build-time static generation and has no support for custom middleware or serving. This can be added in the future to provide a more optimal web experience. In the meantime, authentication can be implemented by using client-side redirects and a loading state.