跳到主要内容
版本:7.x

身份验证流程

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

  • 用户打开应用。
  • 应用从加密的持久存储中加载一些身份验证状态(例如,SecureStore)。
  • 当状态加载完成后,会根据是否加载了有效的身份验证状态,向用户呈现身份验证屏幕或主应用。
  • 当用户退出登录时,我们会清除身份验证状态,并将他们送回身份验证屏幕。
注意

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

我们需要什么

我们希望从身份验证流程中获得以下行为

  • 当用户已登录时,我们希望显示主应用屏幕,而不是与身份验证相关的屏幕。
  • 当用户已登出时,我们希望显示身份验证屏幕,而不是主应用屏幕。
  • 在用户完成身份验证流程并登录后,我们希望卸载所有与身份验证相关的屏幕,并且当我们按下硬件返回按钮时,我们希望无法返回到身份验证流程。

它将如何工作

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

要做到这一点,我们需要几件事

  1. 定义两个 Hook:useIsSignedInuseIsSignedOut,它们返回一个布尔值,指示用户是否已登录。
  2. useIsSignedInuseIsSignedOutif 属性一起使用,以根据条件定义可用的屏幕。

这告诉 React Navigation 根据登录状态显示特定的屏幕。当登录状态更改时,React Navigation 将自动显示适当的屏幕。

定义 Hook

为了实现 useIsSignedInuseIsSignedOut Hook,我们可以从创建一个上下文来存储身份验证状态开始。我们将其称为 SignInContext

import * as React from 'react';

const SignInContext = React.createContext();

然后我们可以按如下方式实现 useIsSignedInuseIsSignedOut Hook

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

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

我们稍后将讨论如何暴露上下文值。

const RootStack = createNativeStackNavigator({
screens: {
LoggedIn: {
if: useIsSignedIn,
screen: signedInStack,
options: {
headerShown: false,
},
},
LoggedOut: {
if: useIsSignedOut,
screen: signedOutStack,
options: {
headerShown: false,
},
},
},
});
Snack 上尝试

定义我们的屏幕

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

  • SplashScreen - 这将在我们恢复令牌时显示启动画面或加载屏幕。
  • SignIn - 如果用户尚未登录(我们找不到令牌),则显示此屏幕。
  • Home - 如果用户已登录,则显示此屏幕。

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

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

const Navigation = createStaticNavigation(RootStack);

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

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

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 中有令牌。这通常可以通过检查我们在 SecureStore 中是否有令牌并验证令牌来完成。在我们获得令牌并且令牌有效后,我们需要设置 userToken。我们还有另一个名为 isSignout 的状态,以便在退出登录时具有不同的动画。

接下来,我们通过 SignInContext 暴露登录状态,以便 useIsSignedInuseIsSignedOut Hook 可以使用它。

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

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,
},
},
},
});
提示

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

在上面的代码片段中,isLoading 意味着我们仍在检查我们是否在 SecureStore 中有令牌。这通常可以通过检查我们在 SecureStore 中是否有令牌并验证令牌来完成。在我们获得令牌并且令牌有效后,我们需要设置 userToken。我们还有另一个名为 isSignout 的状态,以便在退出登录时具有不同的动画。

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

  • 只有当 userTokennull(用户未登录)时才定义 SignIn 屏幕
  • 只有当 userToken 为非 null(用户已登录)时才定义 Home 屏幕

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

const SignInContext = React.createContext();

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

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

/* content */

export default function App() {
/* content */

const isSignedIn = userToken != null;

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

实现恢复令牌的逻辑

注意

以下只是你可能如何在应用中实现身份验证逻辑的示例。你不需要完全按照它进行操作。

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

  • isLoading - 当我们尝试检查我们是否已在 SecureStore 中保存令牌时,我们将其设置为 true
  • isSignout - 当用户正在退出登录时,我们将其设置为 true,否则设置为 false
  • userToken - 用户的令牌。如果它为非 null,我们假设用户已登录,否则未登录。

因此我们需要

  • 添加一些用于恢复令牌、登录和退出的逻辑
  • 向其他组件公开用于登录和退出的方法

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

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

import * as React from 'react';

const AuthContext = React.createContext();

在我们的组件中,我们将

  • 将令牌和加载状态存储在 useReducer
  • 将其持久化到 SecureStore 并在应用启动时从中读取
  • 使用 AuthContext 向子组件公开用于登录和退出的方法

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

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: {
screen: SignInScreen,
options: {
title: 'Sign in',
},
if: useIsSignedOut,
},
},
});

const Navigation = createStaticNavigation(RootStack);
Snack 上尝试

填充其他组件

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

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

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

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

考虑以下示例

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

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

为了使这项工作正常进行,我们可以将 Help 屏幕移动到两个组中,而不是将其放在外部。这将确保当登录状态更改时,屏幕的 navigationKey(组的名称)会更改。

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

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

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

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