大部分应用需要某种方式的用户认证,以便用户可以访问个人相关的数据或其它隐私数据。典型的流程是这样的:
用户打开app
app从加密的持久存储中加载认证信息
加载完以后,根据用户认证状态,用户会跳转到认证页面,或者到app主页面。
如果用户退出登录,我们清理用户的认证信息,并且跳转回认证页面。
注意:这里说的认证页面,就是指跟登录相关的页面,比如登录、注册、忘记密码等。
想要的结果 我们想要的认证流程:当用户登录完以后,我们就要完全抛弃认证状态相关的流程,并且清楚对应的认证页面。及时用户通过物理键后退,我们希望用户不会再次跳转到认证流程。
如何实现 我们可以基于不同的条件定义不同的页面。例如,如果用户已经登录,我们定义Home, Profile, Settings等页面。如果用户没登录,定义SignIn和SignUp等页面。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 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页面;而当isSignedIn是false时,只能看到SignIn和SignUp两个页面。这就保证了用户没有登录时,无论如何不会跳转到Home, Progile和Settings等页面;而用户登录后,也不会跳转到SignIn和SignUp等页面。
这种模式已经被其它路由库(比如Ract Router)使用了很长时间,也被称作“受保护路由”。这里,那些需要用户登录的页面被“保护”着,如果用户没有登录,就无法导航到这些页面。
所有行为会随着IsSignedIn的值变化而改变。这里我们假设isSignedIn初始值为false. 也就意味着,要么SignIn或SignUp会显示。当用户登录以后,isSignedIn的值变成true。这时从React Navigation角度来看,SignIn和SignUp是未定义的,react navigation会将他们从导航栈中删除。然后会显示Home页面,因为你isSignedIn为true时Home页面为导航栈里的第一个页面。
这里是以导航栈为例来说的,但是同样的逻辑同样适用于其他导航器。
通过变量的不同值来定义不同的页面,我们可以用很简单的方式实现认证流程,整个过程不需要额外的逻辑来处理页面的显示。
条件渲染页面时,不要手动跳转 很重要的一点,如果使用了上边例子中的方式,就不要通过navigation.navigete(‘Home’)或其它手工方式跳转到Home页面。当isSigned值变化时,React Navigation会自动实现页面跳转 - 当isSignedIn变成true时显示Home页面,为false时跳转到SignIn页面。如果使用了手工方式导航页面,会产生错误。
页面定义 在我们的导航器中,我们可以根据不同条件定义不同页面。这里我们假设有3个页面:
SplashScreen - 加载token时的启动或加载页面。
SignedInScreen - 用户没有登录时我们要显示的登录页面。
HomeScreen - 用户登录以后要显示的主页。
所以我们的导航器是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 if (state.isLoading ) { 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表示我们仍然在检测token是否存在。一般会检测SecureStore里是否有token,并且验证token的合法性。拿到token并且验证以后,我们需要设置userToken. 我们也可以有另外一个状态isSignout来定义登出时的不同效果。
这里需要重点关注的一点就是,我们根据state变量来定义不同的页面。
SignIn页面只有在userToken为null(用户未登录)时才被定义。
Home页面只有在userToken非null(用户已登录)时才被定义。
这里我们根据条件为每种情况只定义了一个页面。但是你也可以定义多页页面。例如,用户未登录时我们可能还需要定义重置密码、注册等页面。同样的,登录以后也不只一个页面。我们可以用React.Fragment来定义多个页面:
1 2 3 4 5 6 7 8 9 10 11 12 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} /> </> );
注意,如果在栈导航器中既有登录相关页面,也有与登录不相关的页面,我们推荐使用一个导航栈,然后在导航栈中根据条件来定义不同页面,而不是根据条件定义两个不同的导航器。这样可以实现登录和登出之间页面的跳转。
还原token的实现逻辑 注意:下边只是一个在app中实现认证逻辑的例子,你不需要完全准守其中的流程。
从之前的代码段,可以看到我们需要3个state变量
isLoading - 当正在检查SecureStore中是否有token时设置为true。
isSignout - 用户登出时设置为true,否则为false。
userToken - 用户token。如果为非null时我们假设用户已经登录,否则未登录。
因此接下来我们需要:
添加获取token的逻辑,以及登录和登出的逻辑
为其它组件导出登录和登出的方法
这个教程里我们将使用React.useReducer和React.useContext。如果你已经在使用其它状态管理库(比如Redux或Mobx),你可以使用对应的方法实现对应的功能。事实上,在一个大点的app中,全局状态管理库更适合用来保存认证相关的token。同样的方法也适用于这些状态管理库。
首先我们为auth创建一个context:
1 2 3 import * as React from 'react' ;const AuthContext = React .createContext ();
然后我们的组件是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 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 (() => { const bootstrapAsync = async ( ) => { let userToken; try { userToken = await SecureStore .getItemAsync ('userToken' ); } catch (e) { } dispatch ({ type : 'RESTORE_TOKEN' , token : userToken }); }; bootstrapAsync (); }, []); const authContext = React .useMemo ( () => ({ signIn : async data => { dispatch ({ type : 'SIGN_IN' , token : 'dummy-auth-token' }); }, signOut : () => dispatch ({ type : 'SIGN_OUT' }), signUp : async data => { 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 > ); }
完成其它组件 在认证页面怎么实现文本输入和按钮认证不是这里要讨论的范围,这里只是简单放置一些占位函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 > ); }