認証フロー
ほとんどのアプリでは、ユーザーに関連付けられたデータやその他のプライベートコンテンツにアクセスするために、何らかの方法でユーザーを認証する必要があります。 通常、フローは次のようになります。
- ユーザーがアプリを開きます。
- アプリは、暗号化された永続ストレージ(たとえば、
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
の場合は、SignIn
画面とSignUp
画面を認識します。 これにより、ユーザーがサインインしていないときにHome
、Profile
、Settings
画面に移動したり、ユーザーがサインインしているときにSignIn
画面とSignUp
画面に移動したりすることができなくなります。
このパターンは、React Routerなどの他のルーティングライブラリで長い間使用されており、「保護されたルート」として一般的に知られています。 ここでは、ユーザーがサインインする必要がある画面は「保護」されており、ユーザーがサインインしていない場合は他の方法で移動することはできません。
isSignedIn
変数の値が変更されると、魔法が起こります。 最初にisSignedIn
がfalse
だとしましょう。 これは、SignIn
画面またはSignUp
画面が表示されていることを意味します。 ユーザーがサインインした後、isSignedIn
の値はtrue
に変更されます。 React Navigationは、SignIn
画面とSignUp
画面が定義されなくなったことを認識し、それらを削除します。 その後、isSignedIn
がtrue
の場合に定義された最初の画面であるため、自動的にHome
画面が表示されます。
この例ではスタックナビゲーターを示していますが、同じアプローチをどのナビゲーターでも使用できます。
変数に基づいて異なる画面を条件付きで定義することにより、正しい画面が表示されることを確認するための追加のロジックを必要としない簡単な方法で認証フローを実装できます。
条件付きで画面をレンダリングする場合は、手動でナビゲートしないでください
このような設定を使用する場合、navigation.navigate('Home')
またはその他のメソッドを呼び出してHome
画面に**手動で移動しない**ことが重要です。 isSignedIn
が変更されると、**React Navigationは自動的に正しい画面に移動します**。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以外(ユーザーがサインインしている)の場合にのみ定義されます。
ここでは、ケースごとに1つの画面を条件付きで定義しています.しかし、複数の画面を定義することもできます.たとえば、ユーザーがサインインしていない場合は、パスワードのリセット、サインアップなどの画面も定義する必要があります。 同様に、サインイン後にアクセスできる画面には、複数の画面がある可能性があります。 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} />
</>
);
ログイン関連の画面と残りの画面の両方が2つの異なるスタックナビゲーターにある場合は、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();
そのため、コンポーネントは次のようになります。
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
画面にとどまることを意味します。
これは問題になる可能性があります。ユーザーをHelp
画面に留めておくのではなく、SignIn
画面またはHome
画面に移動することをお勧めします。 これを実現するには、navigationKey
propを使用できます。 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>
</>