Skip to main content
Version: 7.x

身份验证流程

大多数应用要求用户以某种方式进行身份验证才能访问与用户或其他私有内容关联的数据。通常流程如下所示:

¥Most apps require that a user authenticates in some way to have access to data associated with a user or other private content. Typically the flow will look like this:

  • 用户打开应用。

    ¥The user opens the app.

  • 该应用从加密的持久存储(例如,SecureStore)加载一些身份验证状态。

    ¥The app loads some authentication state from encrypted persistent storage (for example, SecureStore).

  • 状态加载后,用户会看到身份验证屏幕或主应用,具体取决于是否加载了有效的身份验证状态。

    ¥When the state has loaded, the user is presented with either authentication screens or the main app, depending on whether valid authentication state was loaded.

  • 当用户退出时,我们清除身份验证状态并将其发送回身份验证屏幕。

    ¥When the user signs out, we clear the authentication state and send them back to authentication screens.

注意

我们说 "身份验证屏幕" 是因为通常有多个。你可能有一个带有用户名和密码字段的主屏幕,另一个用于 "忘记密码",另一组用于注册。

¥We say "authentication screens" because usually there is more than one. You may have a main screen with a username and password field, another for "forgot password", and another set for sign up.

我们需要的

¥What we need

我们希望我们的身份验证流程具有以下行为:

¥We want the following behavior from our authentication flow:

  • 当用户登录时,我们希望显示主应用屏幕而不是与身份验证相关的屏幕。

    ¥When the user is signed in, we want to show the main app screens and not the authentication-related screens.

  • 当用户退出时,我们希望显示身份验证屏幕而不是主应用屏幕。

    ¥When the user is signed out, we want to show the authentication screens and not the main app screens.

  • 在用户完成身份验证流程并登录后,我们希望卸载所有与身份验证相关的屏幕,当我们按下硬件后退按钮时,我们预计无法返回到身份验证流程。

    ¥After the user goes through the authentication flow and signs in, we want to unmount all of the screens related to authentication, and when we press the hardware back button, we expect to not be able to go back to the authentication flow.

它将如何运作

¥How it will work

我们可以根据某些条件配置不同的屏幕。例如,如果用户已登录,我们希望 Home 可用。如果用户未登录,我们希望 SignIn 可用。

¥We can configure different screens to be available based on some condition. For example, if the user is signed in, we want Home to be available. If the user is not signed in, we want SignIn to be available.

const RootStack = createNativeStackNavigator({
screens: {
Home: {
if: useIsSignedIn,
screen: HomeScreen,
},
SignIn: {
if: useIsSignedOut,
screen: SignInScreen,
},
},
});
Try on Snack

这里,我们为每个屏幕定义了一个条件,该条件使用接受一个 Hook 的 if 属性。该钩子返回一个布尔值,指示用户是否已登录。如果钩子返回 true,则屏幕可用;否则,屏幕不可用。

¥Here, for each screen, we have defined a condition using the if property which takes a hook. The hook returns a boolean value indicating whether the user is signed in or not. If the hook returns true, the screen will be available, otherwise it won't.

这意味着:

¥This means:

  • useIsSignedIn 返回 true 时,React Navigation 将仅使用 Home 屏幕,因为它是唯一符合条件的屏幕。

    ¥When useIsSignedIn returns true, React Navigation will only use the Home screen, since it's the only screen matching the condition.

  • 同样,当 useIsSignedOut 返回 true 时,React Navigation 将使用 SignIn 屏幕。

    ¥Similarly, when useIsSignedOut returns true, React Navigation will use the SignIn screen.

这使得用户未登录时无法导航到 Home,用户登录时无法导航到 SignIn

¥This makes it impossible to navigate to the Home when the user is not signed in, and to SignIn when the user is signed in.

useIsSignedinuseIsSignedOut 返回的值发生变化时,符合该条件的屏幕也会随之改变:

¥When the values returned by useIsSignedin and useIsSignedOut change, the screens matching the condition will change:

  • 假设最初 useIsSignedOut 返回 true。这意味着会显示 SignIn 屏幕。

    ¥Let's say, initially useIsSignedOut returns true. This means that SignIn screens is shown.

  • 用户登录后,useIsSignedIn 的返回值将变为 trueuseIsSignedOut 的返回值将变为 false,这意味着:

    ¥After the user signs in, the return value of useIsSignedIn will change to true and useIsSignedOut will change to false, which means:

    • React Navigation 会检测到 SignIn 屏幕不再符合条件,因此会将其移除。

      ¥React Navigation will see that the SignIn screen is no longer matches the condition, so it will remove the screen.

    • 然后它会自动显示 Home 屏幕,因为当 useIsSignedIn 返回 true 时,这是第一个可用的屏幕。

      ¥Then it'll show the Home screen automatically because that's the first screen available when useIsSignedIn returns true.

当有多个屏幕符合条件时,屏幕的顺序很重要。例如,如果有两个屏幕与 useIsSignedIn 匹配,当条件为 true 时,将显示第一个屏幕。

¥The order of the screens matters when there are multiple screens matching the condition. For example, if there are two screens matching useIsSignedIn, the first screen will be shown when the condition is true.

定义钩子

¥Define the hooks

要实现 useIsSignedInuseIsSignedOut 钩子,我们可以先创建一个上下文来存储身份验证状态。我们称之为 SignInContext

¥To implement the useIsSignedIn and useIsSignedOut hooks, we can start by creating a context to store the authentication state. Let's call it SignInContext:

import * as React from 'react';

const SignInContext = React.createContext();

然后我们可以按如下方式实现 useIsSignedInuseIsSignedOut 钩子:

¥Then we can implement the useIsSignedIn and useIsSignedOut hooks as follows:

function useIsSignedIn() {
const isSignedIn = React.useContext(SignInContext);
return isSignedIn;
}

function useIsSignedOut() {
return !useIsSignedIn();
}

稍后我们将讨论如何提供上下文值。

¥We'll discuss how to provide the context value later.

添加更多屏幕

¥Add more screens

对于我们的例子,假设我们有 3 个屏幕:

¥For our case, let's say we have 3 screens:

  • SplashScreen - 当我们恢复令牌时,这将显示启动屏幕或加载屏幕。

    ¥SplashScreen - This will show a splash or loading screen when we're restoring the token.

  • SignIn - 这是我们在用户尚未登录时显示的屏幕(我们找不到令牌)。

    ¥SignIn - This is the screen we show if the user isn't signed in already (we couldn't find a token).

  • Home - 这是用户已登录时显示的屏幕。

    ¥Home - This is the screen we show if the user is already signed in.

所以我们的导航器将如下所示:

¥So our navigator will look like:

const RootStack = createNativeStackNavigator({
screens: {
Home: {
if: useIsSignedIn,
screen: HomeScreen,
},
SignIn: {
if: useIsSignedOut,
screen: SignInScreen,
options: {
title: 'Sign in',
},
},
},
});

const Navigation = createStaticNavigation(RootStack);

请注意,我们在这里只定义了 HomeSignIn 屏幕,而不是 SplashScreen。在我们渲染任何导航器之前,应该先渲染 SplashScreen,这样我们就不会在知道用户是否登录之前渲染错误的屏幕。

¥Notice how we have only defined the Home and SignIn screens here, and not the SplashScreen. The SplashScreen should be rendered before we render any navigators so that we don't render incorrect screens before we know whether the user is signed in or not.

当我们在组件中使用它时,它看起来像这样:

¥When we use this in our component, it'd look something like this:

if (isLoading) {
// We haven't finished checking for the token yet
return <SplashScreen />;
}

const isSignedIn = userToken != null;

return (
<SignInContext.Provider value={isSignedIn}>
<Navigation />
</SignInContext.Provider>
);

在上面的代码片段中,isLoading 意味着我们仍在检查是否有令牌。这通常可以通过检查 SecureStore 中是否有令牌并验证该令牌来完成。

¥In the above snippet, isLoading means that we're still checking if we have a token. This can usually be done by checking if we have a token in SecureStore and validating the token.

接下来,我们将通过 SignInContext 公开登录状态,以便 useIsSignedInuseIsSignedOut 钩子可以使用它。

¥Next, we're exposing the sign in status via the SignInContext so that it's available to the useIsSignedIn and useIsSignedOut hooks.

在上面的例子中,我们为每种情况都设置了一个屏幕。但你也可以定义多个屏幕。例如,你可能还想在用户未登录时定义密码重置、注册等屏幕。同样,对于登录后可访问的屏幕,你可能有多个屏幕。

¥In the above example, we have one screen for each case. But you could also define multiple screens. For example, you probably want to define password reset, signup, etc screens as well when the user isn't signed in. Similarly for the screens accessible after sign in, you probably have more than one screen.

我们可以使用 groups 来定义多个屏幕:

¥We can use groups to define multiple screens:

const RootStack = createNativeStackNavigator({
screens: {
// Common screens
},
groups: {
SignedIn: {
if: useIsSignedIn,
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
},
SignedOut: {
if: useIsSignedOut,
screens: {
SignIn: SignInScreen,
SignUp: SignUpScreen,
ResetPassword: ResetPasswordScreen,
},
},
},
});

实现恢复 token 的逻辑

¥Implement the logic for restoring the token

注意

以下只是如何在应用中实现身份验证逻辑的示例。你不需要照原样遵循它。

¥The following is just an example of how you might implement the logic for authentication in your app. You don't need to follow it as is.

从前面的代码片段中,我们可以看到我们需要 3 个状态变量:

¥From the previous snippet, we can see that we need 3 state variables:

  • isLoading - 当我们尝试检查是否已在 SecureStore 中保存了令牌时,我们将其设置为 true

    ¥isLoading - We set this to true when we're trying to check if we already have a token saved in SecureStore.

  • isSignout - 当用户注销时,我们将其设置为 true;否则,设置为 false。这可用于自定义注销时的动画。

    ¥isSignout - We set this to true when user is signing out, otherwise set it to false. This can be used to customize the animation when signing out.

  • userToken - 用户的令牌。如果它非空,我们假设用户已登录,否则没有。

    ¥userToken - The token for the user. If it's non-null, we assume the user is logged in, otherwise not.

所以我们需要:

¥So we need to:

  • 添加一些用于恢复令牌、登录和注销的逻辑

    ¥Add some logic for restoring token, signing in and signing out

  • 向其他组件公开登录和注销的方法

    ¥Expose methods for signing in and signing out to other components

我们将在本指南中使用 React.useReducerReact.useContext。但如果你使用 Redux 或 Mobx 等状态管理库,则可以使用它们来实现此功能。事实上,在较大的应用中,全局状态管理库更适合存储身份验证令牌。你可以将相同的方法应用于你的状态管理库。

¥We'll use React.useReducer and React.useContext in this guide. But if you're using a state management library such as Redux or Mobx, you can use them for this functionality instead. In fact, in bigger apps, a global state management library is more suitable for storing authentication tokens. You can adapt the same approach to your state management library.

首先,我们需要创建一个 auth 上下文,我们可以在其中公开必要的方法:

¥First we'll need to create a context for auth where we can expose the necessary methods:

import * as React from 'react';

const AuthContext = React.createContext();

在我们的组件中,我们将:

¥In our component, we will:

  • 将令牌和加载状态存储在 useReducer

    ¥Store the token and loading state in useReducer

  • 将其保留到 SecureStore 并在应用启动时从那里读取

    ¥Persist it to SecureStore and read it from there on app launch

  • 使用 AuthContext 向子组件公开登录和退出的方法

    ¥Expose the methods for sign in and sign out to child components using AuthContext

所以我们的组件将如下所示:

¥So our component will look like this:

import * as React from 'react';
import * as SecureStore from 'expo-secure-store';

export default function App() {
const [state, dispatch] = React.useReducer(
(prevState, action) => {
switch (action.type) {
case 'RESTORE_TOKEN':
return {
...prevState,
userToken: action.token,
isLoading: false,
};
case 'SIGN_IN':
return {
...prevState,
isSignout: false,
userToken: action.token,
};
case 'SIGN_OUT':
return {
...prevState,
isSignout: true,
userToken: null,
};
}
},
{
isLoading: true,
isSignout: false,
userToken: null,
}
);

React.useEffect(() => {
// Fetch the token from storage then navigate to our appropriate place
const bootstrapAsync = async () => {
let userToken;

try {
// Restore token stored in `SecureStore` or any other encrypted storage
userToken = await SecureStore.getItemAsync('userToken');
} catch (e) {
// Restoring token failed
}

// After restoring token, we may need to validate it in production apps

// This will switch to the App screen or Auth screen and this loading
// screen will be unmounted and thrown away.
dispatch({ type: 'RESTORE_TOKEN', token: userToken });
};

bootstrapAsync();
}, []);

const authContext = React.useMemo(
() => ({
signIn: async (data) => {
// In a production app, we need to send some data (usually username, password) to server and get a token
// We will also need to handle errors if sign in failed
// After getting token, we need to persist the token using `SecureStore` or any other encrypted storage
// In the example, we'll use a dummy token

dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
signOut: () => dispatch({ type: 'SIGN_OUT' }),
signUp: async (data) => {
// In a production app, we need to send user data to server and get a token
// We will also need to handle errors if sign up failed
// After getting token, we need to persist the token using `SecureStore` or any other encrypted storage
// In the example, we'll use a dummy token

dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
},
}),
[]
);

if (state.isLoading) {
// We haven't finished checking for the token yet
return <SplashScreen />;
}

const isSignedIn = state.userToken != null;

return (
<AuthContext.Provider value={authContext}>
<SignInContext.Provider value={isSignedIn}>
<Navigation />
</SignInContext.Provider>
</AuthContext.Provider>
);
}

const RootStack = createNativeStackNavigator({
screens: {
Home: {
if: useIsSignedIn,
screen: HomeScreen,
},
SignIn: {
if: useIsSignedOut,
screen: SignInScreen,
options: {
title: 'Sign in',
},
},
},
});

const Navigation = createStaticNavigation(RootStack);
Try on Snack

填写其他组件

¥Fill in other components

我们不会讨论如何实现身份验证屏幕的文本输入和按钮,这超出了导航的范围。我们只需填写一些占位符内容即可。

¥We won't talk about how to implement the text inputs and buttons for the authentication screen, that is outside of the scope of navigation. We'll just fill in some placeholder content.

function SignInScreen() {
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');

const { signIn } = React.useContext(AuthContext);

return (
<View>
<TextInput
placeholder="Username"
value={username}
onChangeText={setUsername}
/>
<TextInput
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<Button onPress={() => signIn({ username, password })}>Sign in</Button>
</View>
);
}

你可以根据你的要求类似地填写其他屏幕。

¥You can similarly fill in the other screens according to your requirements.

当身份验证状态更改时删除共享屏幕

¥Removing shared screens when auth state changes

考虑以下示例:

¥Consider the following example:

const RootStack = createNativeStackNavigator({
groups: {
LoggedIn: {
if: useIsSignedIn,
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
},
},
LoggedOut: {
if: useIsSignedOut,
screens: {
SignIn: SignInScreen,
SignUp: SignUpScreen,
},
},
},
screens: {
Help: HelpScreen,
},
});

这里我们有特定的屏幕,例如 SignInHome 等,这些屏幕仅根据登录状态显示。但我们还有 Help 屏幕,无论登录状态如何都可以显示。这也意味着,如果用户在 Help 屏幕时登录状态发生变化,他们将停留在 Help 屏幕上。

¥Here we have specific screens such as SignIn, Home etc. which are only shown depending on the sign in state. But we also have the Help screen which can be shown regardless of the login status. This also means that if the sign in state changes when the user is in the Help screen, they'll stay on the Help screen.

这可能是一个问题,我们可能希望将用户带到 SignIn 屏幕或 Home 屏幕,而不是让他们保留在 Help 屏幕上。

¥This can be a problem, we probably want the user to be taken to the SignIn screen or Home screen instead of keeping them on the Help screen.

为了实现这一点,我们可以将 Help 屏幕移动到两个组,而不是将其放在外面。这将确保当登录状态发生变化时,屏幕的 navigationKey(组的名称)也会发生变化。

¥To make this work, we can move the Help screen to both of the groups instead of keeping it outside. This will ensure that the navigationKey (the name of the group) for the screen changes when the sign in state changes.

因此,我们更新后的代码将如下所示:

¥So our updated code will look like the following:

const RootStack = createNativeStackNavigator({
groups: {
LoggedIn: {
if: useIsSignedIn,
screens: {
Home: HomeScreen,
Profile: ProfileScreen,
Help: HelpScreen,
},
},
LoggedOut: {
if: useIsSignedOut,
screens: {
SignIn: SignInScreen,
SignUp: SignUpScreen,
Help: HelpScreen,
},
},
},
});

以上示例展示了堆栈导航器,但你可以将相同的方法应用于任何导航器。

¥The examples above show stack navigator, but you can use the same approach with any navigator.

通过为屏幕指定条件,我们可以以一种简单的方式实现身份验证流程,无需额外的逻辑来确保显示正确的屏幕。

¥By specifying a condition for our screens, we can implement auth flow in a simple way that doesn't require additional logic to make sure that the correct screen is shown.

有条件渲染屏幕时不要手动导航

¥Don't manually navigate when conditionally rendering screens

需要注意的是,使用此类设置时,你不会通过调用 navigation.navigate('Home') 或任何其他方法手动导航到 Home 屏幕。当 isSignedIn 改变时,React Navigation 会自动导航到正确的屏幕 - 当 isSignedIn 变为 true 时,显示 Home 屏幕;当 isSignedIn 变为 false 时,显示 SignIn 屏幕。如果你尝试手动导航,则会收到错误消息。

¥It's important to note that when using such a setup, you don't manually navigate to the Home screen by calling navigation.navigate('Home') or any other method. React Navigation will automatically navigate to the correct screen when isSignedIn changes - Home screen when isSignedIn becomes true, and to SignIn screen when isSignedIn becomes false. You'll get an error if you attempt to navigate manually.

¥Handling deep links after auth

使用深度链接时,你可能需要处理用户打开需要身份验证的深度链接的情况。

¥When using deep links, you may want to handle the case where the user opens a deep link that requires authentication.

示例场景:

¥Example scenario:

  • 用户打开了指向 myapp://profile 的深度链接,但未登录。

    ¥User opens a deep link to myapp://profile but is not signed in.

  • 应用显示 SignIn 屏幕。

    ¥The app shows the SignIn screen.

  • 用户登录后,你希望将其导航到 Profile 屏幕。

    ¥After the user signs in, you want to navigate them to the Profile screen.

要实现这一点,你可以将 UNSTABLE_routeNamesChangeBehavior 设置为 "lastUnhandled"

¥To achieve this, you can set UNSTABLE_routeNamesChangeBehavior to "lastUnhandled":

警告

此 API 为实验性 API,可能会在小版本更新中发生更改。

¥This API is experimental and may change in a minor release.

const RootStack = createNativeStackNavigator({
UNSTABLE_routeNamesChangeBehavior: 'lastUnhandled',
screens: {
Home: {
if: useIsSignedIn,
screen: HomeScreen,
},
SignIn: {
if: useIsSignedOut,
screen: SignInScreen,
options: {
title: 'Sign in',
},
},
},
});

UNSTABLE_routeNamesChangeBehavior 选项允许你控制 React Navigation 在可用屏幕因身份验证状态等条件而发生变化时如何处理导航。当指定 lastUnhandled 时,React Navigation 会记住上次无法处理的屏幕,并在条件改变后,如果该屏幕可用,则会自动导航到该屏幕。

¥The UNSTABLE_routeNamesChangeBehavior option allows you to control how React Navigation handles navigation when the available screens change because of conditions such as authentication state. When lastUnhandled is specified, React Navigation will remember the last screen that couldn't be handled, and after the condition changes, it'll automatically navigate to that screen if it's now available.