身份验证流程
大多数应用程序要求用户以某种方式进行身份验证,才能访问与用户或其他私有内容关联的数据。 通常,流程如下所示
- 用户打开应用程序。
- 应用程序从加密的持久存储加载一些身份验证状态(例如,
SecureStore
)。 - 状态加载后,用户将看到身份验证屏幕或主应用程序,具体取决于是否加载了有效的身份验证状态。
- 当用户注销时,我们将清除身份验证状态并将他们送回身份验证屏幕。
我们称之为“身份验证屏幕”,因为通常不止一个。您可能有一个带有用户名和密码字段的主屏幕,另一个用于“忘记密码”,还有一个用于注册。
我们需要
我们希望身份验证流程具有以下行为:当用户登录时,我们希望丢弃身份验证流程的状态并卸载所有与身份验证相关的屏幕,当我们按下硬件后退按钮时,我们希望无法返回到身份验证流程。
工作原理
我们可以根据某些条件定义不同的屏幕。例如,如果用户已登录,我们可以定义Home
、Profile
、Settings
等。如果用户未登录,我们可以定义SignIn
和SignUp
屏幕。
例如
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} />
</>
);
当我们这样定义屏幕时,当isSignedIn
为true
时,React Navigation 将只看到Home
、Profile
和Settings
屏幕,当它为false
时,React Navigation 将看到SignIn
和SignUp
屏幕。这使得在用户未登录时无法导航到Home
、Profile
和Settings
屏幕,以及在用户已登录时无法导航到SignIn
和SignUp
屏幕。
这种模式在 React Router 等其他路由库中已经使用很长时间了,并且通常被称为“受保护路由”。在这里,需要用户登录的屏幕是“受保护的”,如果用户未登录,则无法通过其他方式导航到这些屏幕。
当isSignedIn
变量的值发生变化时,就会发生神奇的事情。假设,最初isSignedIn
为false
。这意味着,要么显示SignIn
要么显示SignUp
屏幕。用户登录后,isSignedIn
的值将变为true
。React Navigation 将看到SignIn
和SignUp
屏幕不再定义,因此它将删除它们。然后,它将自动显示Home
屏幕,因为这是isSignedIn
为true
时定义的第一个屏幕。
示例展示了堆栈导航器,但您可以对任何导航器使用相同的方法。
通过根据变量有条件地定义不同的屏幕,我们可以以一种简单的方式实现身份验证流程,而无需额外的逻辑来确保显示正确的屏幕。
在有条件地渲染屏幕时不要手动导航
需要注意的是,当使用这种设置时,您**不要手动导航**到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
屏幕仅在userToken
为null
(用户未登录)时定义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.useReducer
和React.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>
</>