Skip to main content
Version: 6.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

这是我们希望从身份验证流程中获得的行为:当用户登录时,我们希望丢弃身份验证流程的状态并卸载与身份验证相关的所有屏幕,并且当我们按下硬件后退按钮时,我们希望无法返回到身份验证流程。

¥This is the behavior that we want from the authentication flow: when users sign in, we want to throw away the state of the authentication flow and 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

我们可以根据某些条件定义不同的屏幕。例如,如果用户已登录,我们可以定义 HomeProfileSettings 等。如果用户未登录,我们可以定义 SignInSignUp 屏幕。

¥We can define different screens based on some condition. For example, if the user is signed in, we can define Home, Profile, Settings etc. If the user is not signed in, we can define SignIn and SignUp screens.

例如:

¥For example:

isSignedIn ? (
<>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
<Stack.Screen name="Settings" component={SettingsScreen} />
</>
) : (
<>
<Stack.Screen name="SignIn" component={SignInScreen} />
<Stack.Screen name="SignUp" component={SignUpScreen} />
</>
);

当我们这样定义屏幕时,当 isSignedIntrue 时,React Navigation 只会看到 HomeProfileSettings 屏幕,当是 false 时,React Navigation 会看到 SignInSignUp 屏幕。这使得用户未登录时无法导航到 HomeProfileSettings 屏幕,用户登录时无法导航到 SignInSignUp 屏幕。

¥When we define screens like this, when isSignedIn is true, React Navigation will only see the Home, Profile and Settings screens, and when it's false, React Navigation will see the SignIn and SignUp screens. This makes it impossible to navigate to the Home, Profile and Settings screens when the user is not signed in, and to SignIn and SignUp screens when the user is signed in.

这种模式已经被其他路由库(例如 React Router)使用了很长时间,通常被称为 "受保护的路由"。这里,我们需要用户登录的屏幕是 "protected",如果用户未登录,则无法通过其他方式导航到。

¥This pattern has been in use by other routing libraries such as React Router for a long time, and is commonly known as "Protected routes". Here, our screens which need the user to be signed in are "protected" and cannot be navigated to by other means if the user is not signed in.

isSignedIn 变量的值发生变化时,奇迹就会发生。假设最初 isSignedInfalse。这意味着,显示 SignInSignUp 屏幕。用户登录后,isSignedIn 的值将更改为 true。React Navigation 将看到 SignInSignUp 屏幕不再定义,因此它将删除它们。然后它会自动显示 Home 屏幕,因为这是 isSignedIntrue 时定义的第一个屏幕。

¥The magic happens when the value of the isSignedIn variable changes. Let's say, initially isSignedIn is false. This means, either SignIn or SignUp screens are shown. After the user signs in, the value of isSignedIn will change to true. React Navigation will see that the SignIn and SignUp screens are no longer defined and so it will remove them. Then it'll show the Home screen automatically because that's the first screen defined when isSignedIn is true.

该示例显示了堆栈导航器,但你可以对任何导航器使用相同的方法。

¥The example shows stack navigator, but you can use the same approach with any navigator.

通过基于变量有条件地定义不同的屏幕,我们可以以简单的方式实现身份验证流程,不需要额外的逻辑来确保显示正确的屏幕。

¥By conditionally defining different screens based on a variable, 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.

定义我们的屏幕

¥Define our screens

在我们的导航器中,我们可以有条件地定义适当的屏幕。对于我们的例子,假设我们有 3 个屏幕:

¥In our navigator, we can conditionally define appropriate screens. 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.

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

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

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

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

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

¥So our navigator will look like:

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

return (
<Stack.Navigator>
{state.userToken == null ? (
// No token found, user isn't signed in
<Stack.Screen
name="SignIn"
component={SignInScreen}
options={{
title: 'Sign in',
// When logging out, a pop animation feels intuitive
// You can remove this if you want the default 'push' animation
animationTypeForReplace: state.isSignout ? 'pop' : 'push',
}}
/>
) : (
// User is signed in
<Stack.Screen name="Home" component={HomeScreen} />
)}
</Stack.Navigator>
);

在上面的代码片段中,isLoading 意味着我们仍在检查是否有令牌。这通常可以通过检查 SecureStore 中是否有令牌并验证该令牌来完成。获得令牌后,如果它有效,我们需要设置 userToken。我们还有另一个名为 isSignout 的状态,用于在注销时显示不同的动画。

¥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. After we get the token and if it's valid, we need to set the userToken. We also have another state called isSignout to have a different animation on sign out.

需要注意的主要事情是,我们根据这些状态变量有条件地定义屏幕:

¥The main thing to notice is that we're conditionally defining screens based on these state variables:

  • 仅当 userTokennull 时才定义 SignIn 屏幕(用户未登录)

    ¥SignIn screen is only defined if userToken is null (user is not signed in)

  • 仅当 userToken 非空(用户已登录)时才定义 Home 屏幕

    ¥Home screen is only defined if userToken is non-null (user is signed in)

在这里,我们有条件地为每种情况定义一个屏幕。但你也可以定义多个屏幕。例如,你可能还想在用户未登录时定义密码重置、注册等屏幕。同样,对于登录后可访问的屏幕,你可能有多个屏幕。我们可以使用 React.Fragment 来定义多个屏幕:

¥Here, we're conditionally defining 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 signing in, you probably have more than one screen. We can use React.Fragment to define multiple screens:

state.userToken == null ? (
<>
<Stack.Screen name="SignIn" component={SignInScreen} />
<Stack.Screen name="SignUp" component={SignUpScreen} />
<Stack.Screen name="ResetPassword" component={ResetPassword} />
</>
) : (
<>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</>
);
提示

如果你的登录相关屏幕和其余屏幕位于两个不同的 Stack 导航器中,我们建议你使用单个 Stack 导航器并将条件放在内部,而不是使用 2 个不同的导航器。这使得在登录/注销期间可以有适当的过渡动画。

¥If you have both your login-related screens and rest of the screens in two different Stack navigators, we recommend to use a single Stack navigator and place the conditional inside instead of using 2 different navigators. This makes it possible to have a proper transition animation during login/logout.

实现恢复 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

  • 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 necessary methods:

import * as React from 'react';

const AuthContext = React.createContext();

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

¥So our component will look like this:

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

export default function App({ navigation }) {
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 {
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`
// 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`
// In the example, we'll use a dummy token

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

return (
<AuthContext.Provider value={authContext}>
<Stack.Navigator>
{state.userToken == null ? (
<Stack.Screen name="SignIn" component={SignInScreen} />
) : (
<Stack.Screen name="Home" component={HomeScreen} />
)}
</Stack.Navigator>
</AuthContext.Provider>
);
}

填写其他组件

¥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 title="Sign in" onPress={() => signIn({ username, password })} />
</View>
);
}

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

¥Removing shared screens when auth state changes

考虑以下示例:

¥Consider the following example:

isSignedIn ? (
<>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
<Stack.Screen name="Help" component={HelpScreen} />
</>
) : (
<>
<Stack.Screen name="SignIn" component={SignInScreen} />
<Stack.Screen name="SignUp" component={SignUpScreen} />
<Stack.Screen name="Help" component={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 in both cases. This also means that if the signin state changes when the user is in the Help screen, they'll stay on the Help screen.

这可能是一个问题,我们可能希望将用户带到 SignIn 屏幕或 Home 屏幕,而不是让他们保留在 Help 屏幕上。为了完成这项工作,我们可以使用 navigationKey 属性。当 navigationKey 改变时,React Navigation 将移除所有屏幕。

¥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. To make this work, we can use the navigationKey prop. When the navigationKey changes, React Navigation will remove all the screen.

所以我们更新后的代码将如下所示:

¥So our updated code will look like following:

<>
{isSignedIn ? (
<>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</>
) : (
<>
<Stack.Screen name="SignIn" component={SignInScreen} />
<Stack.Screen name="SignUp" component={SignUpScreen} />
</>
)}
<Stack.Screen
navigationKey={isSignedIn ? 'user' : 'guest'}
name="Help"
component={HelpScreen}
/>
</>

如果你有一堆共享屏幕,你还可以使用 navigationKeyGroup 删除组中的所有屏幕。例如:

¥If you have a bunch of shared screens, you can also use navigationKey with a Group to remove all of the screens in the group. For example:

<>
{isSignedIn ? (
<>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</>
) : (
<>
<Stack.Screen name="SignIn" component={SignInScreen} />
<Stack.Screen name="SignUp" component={SignUpScreen} />
</>
)}
<Stack.Group navigationKey={isSignedIn ? 'user' : 'guest'}>
<Stack.Screen name="Help" component={HelpScreen} />
<Stack.Screen name="About" component={AboutScreen} />
</Stack.Group>
</>