React Native Expo急速教程 6 - 认证流程

1,447 阅读6分钟

翻译: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>
  );
}