在 Expo Router 中使用重定向进行身份验证
如何使用 Expo Router 实现身份验证和保护路由。
信息 SDK 53 引入了 受保护的路由,这是一种更强大的身份验证处理方法。如果你使用的是 SDK 52 或更早版本,请按照此指南操作。

学习如何在你的 Expo Router 项目中实现身份验证流程。
使用 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.
使用 React 上下文和路由组
🌐 Using React Context and Route Groups
通常会将特定路由限制为未认证的用户访问。这可以通过使用 React Context 和路由组以有组织的方式实现。考虑以下的项目结构,其中 /sign-in 路由始终可访问,而 (app) 组则需要认证:
🌐 It's common to restrict specific routes to users who are not authenticated. This is achievable in an organized way by using React Context and Route Groups. Consider the following project structure that has a /sign-in route that is always accessible and a (app) group that requires authentication:
app_layout.tsxsign-in.tsxAlways accessible (app)_layout.tsxProtects child routesindex.tsxRequires authorization 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.
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, }); // This hook can be used to access the user info. 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: () => { // 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.
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> { // 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
在根布局中使用 SessionProvider 为整个应用提供身份验证上下文。必须在触发任何导航事件之前挂载 <Slot />,否则会抛出运行时错误。
🌐 Use the SessionProvider in the root layout to provide the authentication context to the entire app. It's imperative that the <Slot /> is mounted before any navigation events are triggered. Otherwise, a runtime error will be thrown.
import { Slot } from 'expo-router'; import { SessionProvider } from '../ctx'; export default function Root() { // Set up the auth context and render our layout inside of it. return ( <SessionProvider> <Slot /> </SessionProvider> ); }
3
创建一个嵌套的 布局路由,在渲染子路由组件之前检查用户是否已通过身份验证。如果用户未通过身份验证,该布局路由会将用户重定向到登录界面。
🌐 Create a nested layout route that checks whether users are authenticated before rendering the child route components. This layout route redirects users to the sign-in screen if they are not authenticated.
import { Text } from 'react-native'; import { Redirect, Stack } from 'expo-router'; import { useSession } from '../../ctx'; export default function AppLayout() { const { session, isLoading } = useSession(); // You can keep the splash screen open, or render a loading screen like we do here. if (isLoading) { return <Text>Loading...</Text>; } // Only require authentication within the (app) group's layout as users // need to be able to access the (auth) group and sign in again. if (!session) { // On web, static rendering will stop here as the user is not authenticated // in the headless Node process that the pages are rendered in. return <Redirect href="/sign-in" />; } // This layout can be deferred because it's not the root layout. return <Stack />; }
4
创建 /sign-in 屏幕。它可以使用 signIn() 切换身份验证。由于此屏幕在 (app) 组之外,渲染此屏幕时不会运行该组的布局和身份验证检查。这允许未登录的用户查看此屏幕。
🌐 Create the /sign-in screen. It can toggle the 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 see this screen.
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
实现一个经过身份验证的界面,让用户可以注销。
🌐 Implement an authenticated screen that lets users sign out.
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 `app/(app)/_layout.tsx` will redirect to the sign-in screen. signOut(); }}> Sign Out </Text> </View> ); }
你现在有一个应用,可以在检查初始身份验证状态时显示加载状态,并在用户未经过身份验证时重定向到登录屏幕。如果用户访问带有身份验证检查的任何路由的深层链接,他们将被重定向到登录屏幕。
🌐 You now have an app that can present a loading state while it checks the initial authentication state and 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.
替代加载状态
🌐 Alternative loading states
使用 Expo Router 时,在加载初始认证状态时必须在屏幕上渲染一些内容。在上面的示例中,应用布局会渲染一个加载消息。或者,你可以将 index 路由设为加载状态,并将初始路由移动到类似 /home 的地方,这与 X 的工作方式类似。
🌐 With Expo Router, something must be rendered to the screen while loading the initial auth state. In the example above, the app layout renders a loading message. Alternatively, you can make the index route a loading state and move the initial route to something such as /home, which is similar to how X works.
模态和每条路由的身份验证
🌐 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.tsxsign-in.tsxModal presented over the root(root)_layout.tsxProtects child routesindex.tsxRequires authorization 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> ); }
无需导航即可导航
🌐 Navigating without navigation
当应用尝试在 根布局 中执行导航但未挂载导航器时,你可能会遇到以下错误。
🌐 You may encounter the following error when the app attempts to perform navigation without a navigator mounted in the root layout.
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.
要解决此问题,请添加一个组并将条件逻辑下移一个级别。
🌐 To fix this, add a group and move conditional logic down a level.
之前
🌐 Before
app_layout.tsxabout.tsxexport default function RootLayout() { React.useEffect(() => { // This navigation event will trigger the error above. router.push('/about'); }, []); // This conditional statement creates a problem since the root layout's // content (the Slot) must be mounted before any navigation events occur. if (isLoading) { return <Text>Loading...</Text>; } return <Slot />; }
之后
🌐 After
app_layout.tsx(app)_layout.tsxMove conditional logic down a levelabout.tsxexport default function RootLayout() { return <Slot />; }
export default function RootLayout() { React.useEffect(() => { router.push('/about'); }, []); // It is OK to defer rendering this nested layout's content. We couldn't // defer rendering the root layout's content since a navigation event (the // redirect) would have been triggered before the root layout's content had // been mounted. if (isLoading) { return <Text>Loading...</Text>; } return <Slot />; }
中间件
🌐 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.