概要
Reactでの認証は、やらないといけないけど、面倒な実装の一つだと思います。
いろんなやり方があるかと思いますが、自分なりに見つけた、Reactでの認証トークンの取り扱いを記載していきたいと思います。
ここの記事では、RESTfulなAPIサーバーにアクセスする前提で、認証にJWT等のトークン情報を用いる場合を対象とします。
この記事の対象は、
- クライアント側でトークン情報の保存
- APIリクエスト時のトークン情報の読み出し
- 認証されたユーザーが見られるページの制御
注意:記載したものはセキュリティ的な懸念が出てきたら随時更新したいと思います。
使うライブラリ
- React
- React Router
認証の流れ
クライアント側でのトークン情報の保存
ユーザーがログイン操作をしたら、サーバーからはJWTが返ってきます。
それをどこに保存すべきか、選択肢は
- Local Storage
- Session Storage
- Cookie
今回は、Javascript・Typescriptで簡単に操作が可能で、タブが閉じるまで生き続けるSessionStorageを保存先とします。
なお、LocalStorageの場合もあまり違いはありません。
また、前提として、複数のタブで同じサービスを開いてログイン操作するなどはしないものとします。
(その場合の実装も後ほど書いてみたいと思います。)
方針:Reactカスタムフックの利用
React カスタムフックでSession Storageの情報を監視・操作します(リアルタイム監視ではないです)
このフックはこちらの記事を参考にさせていただきました。
https://qiita.com/ShimeiYago/items/28f6d088704bf6accf02
const STORAGE_EVENT_KEY = 'authchanged';
import { useCallback, useEffect, useState } from 'react';
export default function useAuthStorage() {
const getStorage = () => {
if (typeof window === 'undefined') return null;
return sessionStorage.getItem('token');
};
const [token, setTokenState] = useState(getStorage);
useEffect(() => {
const handleStorageChange = () => {
setTokenState(getStorage());
};
window.addEventListener(STORAGE_EVENT_KEY, handleStorageChange);
return () => {
window.removeEventListener(STORAGE_EVENT_KEY, handleStorageChange);
};
}, []);
const setToken = useCallback((newToken: string | null) => {
if (typeof window === 'undefined') return;
if (newToken) {
sessionStorage.setItem('token', newToken);
} else {
sessionStorage.removeItem('token');
}
setTokenState(newToken);
window.dispatchEvent(new Event(STORAGE_EVENT_KEY));
}, []);
return { token, setToken };
}
これを使って、
-
認証情報が必要なページでtoken情報をuseAuthStorage()から取り出したり、
-
サインイン時にsetTokenを使って、新しいトークン情報を保存したりできます。
例えば、サインインページでは次のように使ったりします。
export default function Signin() {
const { token, setToken } = useAuthStorage(); // <--- 設定されているトークンと設定用Functionを取得
const handleSubmit = useCallback(
(values: SigninForm) => {
fetch('/v1/api/auth/signin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: values.username,
password: values.password,
}),
})
.then((raw) => raw.json())
.then((d) => setToken(d.token)) // <--- setTokenで新しいトークンを保存
useAuthStorage()をコンポーネントで呼び出せば、設定されているトークンを取得できるので、それを使ってAPIアクセスも可能です。
export default function Signin() {
const { token } = useAuthStorage(); // <--- 設定されているトークンを取得
useEffect(() => {
fetch('/v1/api/sample', {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
});
}, [])
サインアウトをしたい場合は、次のページを開くことできるようにしました。
ReactRouterも併用して、サインアウト後はサインインページに飛ばすようにしています。
export default function Signout() {
const { setToken } = useAuthStorage();
useEffect(() => {
setToken(null);
}, [setToken]);
return <Navigate to="/auth/signin" />;
}
認証されたユーザーが見られるページの制御
認証していないとデータが取得できず、ページがうまく表示されないという状況はあるあるだと思います。
認証していないユーザーがアクセスしたら、サインインページへ飛ばすような流れをReact Routerを使って実装します。
流れ
- 認証されているかを確認するガードコンポーネントの作成
- Routesへの適用
ガードコンポーネントで認証トークンの有無を確認
ガードコンポーネントの役割は、
- 認証トークンがあるかを確認
- トークンがあれば、Outletを利用して、子のRouteコンポーネントを有効化
- トークンがなければ、Navigateを利用して、Sigininページへ飛ばす
export default function AuthedRoutes() {
const { token } = useAuthStorage();
return token ? <Outlet /> : <Navigate to="/auth/signin" replace />;
}
ガードコンポーネントをRouteに反映
認証が必要なパス(ページ)はAuthedRoutesで保護します。
export default function App() {
return (
//...省略
<BrowserRouter>
<Routes>
<Route path="/auth/signup" element={<Signup />} />
<Route path="/auth/signin" element={<Signin />} />
<Route element={<AuthedRoutes />}>
{/* <------------ ここから下は認証トークンが必要(サインインしている必要あり)*/}
<Route path="/" element={<PostNote />} />
<Route path="/post" element={<PostNote />} />
<Route path="/questions" element={<PostQuestions />} />
<Route path="/diaries" element={<Diaries />} />
<Route path="/auth/signout" element={<Signout />} />
</Route>
</Routes>
</BrowserRouter>
//...省略
}
まとめ
トークンは迷わず、ReactカスタムフックでSessionStorageを監視・操作して、ReactRouterで制御する。
一例として参考にしていただけたら幸いです!
関連プロジェクト
