翻译:reactnavigation.org/docs/auth-f… 译者: 刘传君
认证流程
大多数应用程序要求用户以某种方式进行身份验证,以访问与用户相关的数据或其他私人内容。通常情况下,流程会是这样的。
- 用户打开应用程序。
- 应用程序从持久化存储(例如,AsyncStorage)中加载一些认证状态。
- 当状态加载完毕后,根据是否加载了有效的认证状态,用户会看到认证屏幕或主应用。
- 当用户签出时,我们会清除认证状态,并将他们送回登录屏幕。
注意:我们说 "登录屏幕 "是因为通常有多个屏幕。你可能有一个主屏幕,上面有用户名和密码字段,另一个是 "忘记密码",还有一个设置为注册。
我们需要什么
这就是我们希望从登录流程中得到的行为:当用户登录时,我们希望去掉登录过程,解除所有与登录相关的屏幕,当我们按下硬件返回按钮时,我们希望无法回到得登录流程。
如何操作
我们可以根据一些条件来定义不同的屏幕。例如,如果用户是登录状态,我们可以定义主页、个人资料、设置等。如果用户没有登录,我们可以定义登录和登录界面:
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为真时,React Navigation只能看到Home、Profile和Settings屏幕,当isSignedIn为假时,React Navigation将看到SignIn和SignUp屏幕。这就使得当用户未登录时,无法导航到主页、配置文件和设置界面,而当用户登录时,又无法导航到SignIn和SignUp界面。
这种模式已经被其他路由库(如React Router)使用了很久,俗称 "保护路由"。在这里,我们需要用户登录的屏幕是被 "保护 "的,如果用户没有登录,就无法通过其他方式导航到。
神奇的事情发生在isSignedIn变量的值发生变化的时候。比方说,最初isSignedIn是false。这意味着,无论是SignIn还是SignUp屏幕都可以显示。在用户登录后,isSignedIn的值将变为true。React Navigation会不看到定义的SignIn和SignUp屏幕。然后它会自动显示主屏幕,因为当isSignedIn为真时,那是第一个定义的屏幕。
需要注意的是,当使用这样的设置时,你不需要通过调用navigation.navigate('Home')来手动导航到主屏幕。当isSignedIn变为true时,React Navigation将自动导航到主屏幕。
这利用了React Navigation的一个新特性:能够根据属性或状态动态定义和改变导航器的屏幕定义。这个例子显示的是堆栈导航器,但你可以对任何导航器使用同样的方法。
通过基于变量有条件地定义不同的屏幕,我们可以用一种简单的方式实现auth流,不需要额外的逻辑来确保显示正确的屏幕。
定义我们的屏幕
在我们的导航器中,我们可以有条件地定义相应的屏幕。在我们的案例中,假设我们有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意味着我们仍在检查是否有令牌,这通常可以通过检查AsyncStorage中是否有令牌并验证令牌来完成。这通常可以通过检查AsyncStorage中是否有token并验证token来完成。在我们得到token后,如果有效,我们需要设置userToken。我们还有一个状态叫做isSignout,以便在签出时有不同的动画。
需要注意的是,我们是根据这些状态变量来有条件地定义屏幕的。
- SignIn屏幕只有在userToken为空的情况下才会被定义(用户没有登录)。
- 只有在userToken为非空时才会定义主屏幕(用户已登录)。
在这里,我们有条件的为每个案例定义一个屏幕,但你也可以定义多个屏幕。例如,当用户没有登录时,你可能也想定义密码重置、登录等屏幕。同样对于登录后可以访问的屏幕,你可能也有多个屏幕。我们可以使用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} />
</>
);
实现恢复令牌的逻辑
注意:以下只是一个例子,说明您如何在您的应用程序中实现认证逻辑。你不需要按照它的原样来做。
从前面的片段中,我们可以看到我们需要3个状态变量。
- isLoading - 当我们试图检查AsyncStorage中是否已经保存了一个令牌时,我们将其设置为true。
- isSignout - 当用户签出时,我们将其设置为true,否则设置为false。
- userToken - 用户的token。如果它是非空的,我们假设该用户已经登录,否则就不是。 所以我们需要。
添加一些逻辑来恢复token,登录和注销。将登录和登出的方法暴露给其他组件。
我们将在本指南中使用React.useReducer和React.useContext。但如果你正在使用Redux或Mobx等状态管理库,你可以用它们来代替这个功能。事实上,在更大的应用程序中,全局状态管理库更适合存储认证令牌。你可以将同样的方法调整到你的状态管理库中。
首先,我们需要为auth创建一个上下文,在这里我们可以暴露必要的方法。
import * as React from 'react'; const AuthContext = React.createContext();
现在我们的组件看起来是这样的:
import * as React from 'react';
import AsyncStorage from '@react-native-community/async-storage';
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 AsyncStorage.getItem('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 `AsyncStorage`
// 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 `AsyncStorage`
// 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>
);
}