身份验证流程
大多数应用都要求用户以某种方式进行身份验证,才能访问与用户或其他私人内容关联的数据。通常,流程如下所示
- 用户打开应用。
- 应用从加密的持久存储中加载一些身份验证状态(例如,
SecureStore
)。 - 当状态加载完成后,会根据是否加载了有效的身份验证状态,向用户呈现身份验证屏幕或主应用。
- 当用户退出登录时,我们会清除身份验证状态,并将他们送回身份验证屏幕。
我们说“身份验证屏幕”是因为通常不止一个。你可能有一个主屏幕,带有用户名和密码字段,另一个用于“忘记密码”,还有另一组用于注册。
我们需要什么
我们希望从身份验证流程中获得以下行为
- 当用户已登录时,我们希望显示主应用屏幕,而不是与身份验证相关的屏幕。
- 当用户已登出时,我们希望显示身份验证屏幕,而不是主应用屏幕。
- 在用户完成身份验证流程并登录后,我们希望卸载所有与身份验证相关的屏幕,并且当我们按下硬件返回按钮时,我们希望无法返回到身份验证流程。
它将如何工作
我们可以根据某些条件配置可用的不同屏幕。例如,如果用户已登录,我们可以定义 Home
、Profile
、Settings
等。如果用户未登录,我们可以定义 SignIn
和 SignUp
屏幕。
- 静态
- 动态
要做到这一点,我们需要几件事
- 定义两个 Hook:
useIsSignedIn
和useIsSignedOut
,它们返回一个布尔值,指示用户是否已登录。 - 将
useIsSignedIn
和useIsSignedOut
与if
属性一起使用,以根据条件定义可用的屏幕。
这告诉 React Navigation 根据登录状态显示特定的屏幕。当登录状态更改时,React Navigation 将自动显示适当的屏幕。
定义 Hook
为了实现 useIsSignedIn
和 useIsSignedOut
Hook,我们可以从创建一个上下文来存储身份验证状态开始。我们将其称为 SignInContext
import * as React from 'react';
const SignInContext = React.createContext();
然后我们可以按如下方式实现 useIsSignedIn
和 useIsSignedOut
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,
},
},
},
});
例如
{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
时定义的第一个屏幕。
该示例显示了堆栈导航器,但你可以将相同的方法用于任何导航器。
通过根据变量有条件地定义不同的屏幕,我们可以以一种简单的方式实现身份验证流程,而无需额外的逻辑来确保显示正确的屏幕。
定义我们的屏幕
在我们的导航器中,我们可以有条件地定义适当的屏幕。对于我们的情况,假设我们有 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);
请注意,我们在这里只定义了 Home
和 SignIn
屏幕,而没有定义 SplashScreen
。SplashScreen
应该在我们渲染任何导航器之前渲染,这样我们就不会在知道用户是否已登录之前渲染不正确的屏幕。
当我们在组件中使用它时,它看起来像这样
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
暴露登录状态,以便 useIsSignedIn
和 useIsSignedOut
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 个不同的导航器。这使得在登录/注销期间可以进行适当的过渡动画。
if (isLoading) {
// We haven't finished checking for the token yet
return <SplashScreen />;
}
return (
<NavigationContainer>
<Stack.Navigator>
{userToken == null ? (
// No token found, user isn't signed in
<Stack.Screen
name="SignIn"
component={SimpleSignInScreen}
options={{
title: 'Sign in',
}}
initialParams={{ setUserToken }}
/>
) : (
// User is signed in
<Stack.Screen name="Home" component={HomeScreen} />
)}
</Stack.Navigator>
</NavigationContainer>
);
在上面的代码片段中,isLoading
意味着我们仍在检查我们是否在 SecureStore
中有令牌。这通常可以通过检查我们在 SecureStore
中是否有令牌并验证令牌来完成。在我们获得令牌并且令牌有效后,我们需要设置 userToken
。我们还有另一个名为 isSignout
的状态,以便在退出登录时具有不同的动画。
需要注意的主要事项是我们正在根据这些状态变量有条件地定义屏幕
- 只有当
userToken
为null
(用户未登录)时才定义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>
);
}
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} />
</>
);
如果你的登录相关屏幕和其余屏幕在两个不同的堆栈导航器中,我们建议使用单个堆栈导航器并将条件放在内部,而不是使用 2 个不同的导航器。这使得在登录/注销期间可以进行适当的过渡动画。
实现恢复令牌的逻辑
以下只是你可能如何在应用中实现身份验证逻辑的示例。你不需要完全按照它进行操作。
从前面的代码片段中,我们可以看到我们需要 3 个状态变量
isLoading
- 当我们尝试检查我们是否已在SecureStore
中保存令牌时,我们将其设置为true
isSignout
- 当用户正在退出登录时,我们将其设置为true
,否则设置为false
userToken
- 用户的令牌。如果它为非 null,我们假设用户已登录,否则未登录。
因此我们需要
- 添加一些用于恢复令牌、登录和退出的逻辑
- 向其他组件公开用于登录和退出的方法
在本指南中,我们将使用 React.useReducer
和 React.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);
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' });
},
}),
[]
);
return (
<AuthContext.Provider value={authContext}>
<NavigationContainer>
<Stack.Navigator>
{state.isLoading ? (
// We haven't finished checking for the token yet
<Stack.Screen name="Splash" component={SplashScreen} />
) : 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
animationTypeForReplace: state.isSignout ? 'pop' : 'push',
}}
/>
) : (
// User is signed in
<Stack.Screen name="Home" component={HomeScreen} />
)}
</Stack.Navigator>
</NavigationContainer>
</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 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,
},
});
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
屏幕上。
- 静态
- 动态
为了使这项工作正常进行,我们可以将 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,
},
},
},
});
为了使这项工作正常进行,我们可以使用 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>
</>
当有条件地渲染屏幕时,不要手动导航
重要的是要注意,当使用这种设置时,你不要手动导航到 Home
屏幕,方法是调用 navigation.navigate('Home')
或任何其他方法。 当 isSignedIn
更改时,React Navigation 将自动导航到正确的屏幕 - 当 isSignedIn
变为 true
时导航到 Home
屏幕,当 isSignedIn
变为 false
时导航到 SignIn
屏幕。 如果你尝试手动导航,将会收到错误。