跳至主要内容
版本:6.x

身份验证流程

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

  • 用户打开应用程序。
  • 应用程序从加密的持久存储加载一些身份验证状态(例如,SecureStore)。
  • 状态加载后,用户将看到身份验证屏幕或主应用程序,具体取决于是否加载了有效的身份验证状态。
  • 当用户注销时,我们将清除身份验证状态并将他们送回身份验证屏幕。
注意

我们称之为“身份验证屏幕”,因为通常不止一个。您可能有一个带有用户名和密码字段的主屏幕,另一个用于“忘记密码”,还有一个用于注册。

我们需要

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

工作原理

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

例如

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屏幕。

这种模式在 React Router 等其他路由库中已经使用很长时间了,并且通常被称为“受保护路由”。在这里,需要用户登录的屏幕是“受保护的”,如果用户未登录,则无法通过其他方式导航到这些屏幕。

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

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

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

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

需要注意的是,当使用这种设置时,您**不要手动导航**到Home屏幕,方法是调用navigation.navigate('Home')或任何其他方法。**React Navigation将在isSignedIn更改时自动导航到正确的屏幕** - 当isSignedIn变为true时导航到Home屏幕,当isSignedIn变为false时导航到SignIn屏幕。如果您尝试手动导航,将会出现错误。

定义我们的屏幕

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

  • SplashScreen - 当我们正在恢复令牌时,这将显示一个启动或加载屏幕。
  • SignInScreen - 如果用户尚未登录(我们找不到令牌),则会显示此屏幕。
  • HomeScreen - 如果用户已登录,则会显示此屏幕。

因此,我们的导航器将如下所示

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的状态,以便在注销时使用不同的动画。

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

  • SignIn屏幕仅在userTokennull(用户未登录)时定义
  • Home屏幕仅在userToken不为null(用户已登录)时定义

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

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 导航器并将条件放在其中,而不是使用两个不同的导航器。这样可以实现登录/注销时的正常过渡动画。

实现恢复 token 的逻辑

注意

以下是你在应用程序中实现身份验证逻辑的示例。你无需完全按照此示例进行。

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

  • isLoading - 当我们尝试检查是否已在SecureStore中保存了 token 时,将其设置为true
  • isSignout - 当用户注销时,将其设置为true,否则设置为false
  • userToken - 用户的 token。如果它不为空,则表示用户已登录,否则表示未登录。

因此我们需要

  • 添加一些用于恢复 token、登录和注销的逻辑
  • 向其他组件公开登录和注销方法

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

首先,我们需要为身份验证创建一个上下文,以便在其中公开必要的方法

import * as React from 'react';

const AuthContext = React.createContext();

因此,我们的组件将如下所示

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>
);
}

填充其他组件

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

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>
);
}

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

考虑以下示例

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} />
</>
);

这里我们有特定的屏幕,例如 `SignIn`、`Home` 等,它们只在登录状态下显示。但我们也有 `Help` 屏幕,它可以在两种情况下显示。这意味着,如果用户在 `Help` 屏幕上时登录状态发生变化,他们将仍然停留在 `Help` 屏幕上。

这可能是一个问题,我们可能希望用户被带到 `SignIn` 屏幕或 `Home` 屏幕,而不是让他们留在 `Help` 屏幕上。为了实现这一点,我们可以使用 navigationKey 属性。当 `navigationKey` 发生变化时,React Navigation 将移除所有屏幕。

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

<>
{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}
/>
</>

如果你有一堆共享屏幕,你也可以使用 navigationKey 与 `Group` 来移除组中的所有屏幕。例如

<>
{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>
</>