全体像
- CSRFトークン取得(初回アクセス)
- Cookieに保存される
- リクエスト時にCookieから取得
- ヘッダーに付与して送信
1. CSRFトークンを取得する
function App() {
useEffect(() => {
// 初回アクセス時にCSRFトークンを取得
fetch("/api/v1/csrf", {
credentials: "include", // Cookieを受け取るために必要
});
}, []);
return <AppRouter />;
}
export default App;
なぜ App.tsx に書くのか?
CSRFトークンは一度取得すれば使い回せるため、アプリ起動時に取得するのが適切です。
App.tsx は最初にマウントされるルートコンポーネントなので、このような「初回のみ実行する処理」を置くのに適しています。
また、各コンポーネントで取得するとリクエストの重複につながるため、共通の入口でまとめて処理します。
2. Cookieに保存される
export function getCookie(name: string): string | null {
return (
document.cookie
// "key=value; key2=value2" の形式なので分割する
.split("; ")
// 指定したCookie名(例: XSRF-TOKEN)を探す
.find((row) => row.startsWith(name + "="))
// "key=value" から value のみ取得
?.split("=")[1] ?? null // 存在しない場合はnull
);
}
CSRFトークンは、サーバーからレスポンスとして直接返されるのではなく、Cookieとしてブラウザに保存されます。
そのため、リクエスト時にヘッダーへ付与するには、document.cookie からトークンを取得する必要があります。
ただし document.cookie は "key=value; key2=value2" のような文字列で管理されているため、そのままでは扱いづらく、このように分割・検索して目的の値を取り出します。
3. リクエスト時にCookieから取得
export async function apiFetch(url: string, options: RequestInit = {}) {
// CookieからCSRFトークンを取得
const csrfToken = getCookie("XSRF-TOKEN");
const response = await fetch(url, {
...options,
credentials: "include", // Cookieを送受信するために必要
headers: {
"Content-Type": "application/json",
// CSRFトークンをヘッダーに付与
"X-XSRF-TOKEN": csrfToken ?? "",
// 呼び出し元で指定されたヘッダーをマージ
...(options.headers || {}),
},
});
// レスポンスをテキストとして取得(空レスポンス対策)
const text = await response.text();
// 中身がある場合のみJSONパース
const data = text ? JSON.parse(text) : null;
// ステータスがエラーの場合は例外を投げる
if (!response.ok) {
throw new Error(data.error || "リクエストに失敗しました");
}
return data;
}
CSRFトークンは、Cookieに保存されているだけでは不十分で、リクエスト時にヘッダーへ付与する必要があります。
この関数では、まずCookieからトークンを取得し、それを X-XSRF-TOKEN ヘッダーとして送信しています。
サーバー側(例:Spring Security)は、このヘッダーとCookieの値を照合することで、リクエストが正当なものであるかを検証します。
また、この処理を共通関数としてまとめることで、各API呼び出しでCSRFを意識する必要がなくなり、安全性と実装のシンプルさを両立できます。
4. ヘッダーに付与して送信
try {
// 共通関数(apiFetch)を使ってユーザー登録APIを呼び出す
await apiFetch(SIGNUP, {
method: "POST",
// リクエストボディをJSON形式で送信
body: JSON.stringify({
email,
password,
confirmPassword,
}),
});
// ※ CSRFトークンはapiFetch内で自動的に付与される
} catch (e: any) {
// エラーレスポンスのメッセージを画面に表示
setError(e.message);
}
実際のAPI呼び出しでは、特別なCSRF処理を書く必要はありません。
apiFetch を経由することで、Cookieから取得したCSRFトークンが自動的にヘッダーへ付与されます。
そのため、各コンポーネントでは通常のリクエストと同じように実装でき、CSRF対策を意識せずに安全な通信を行うことができます。
おまけ:Spring Security側のCSRFトークン発行
@RestController
public class CsrfController {
@GetMapping("/api/v1/csrf")
public ResponseEntity<Void> loadCsrf(CsrfToken token) {
// トークンを生成(Cookieにセットされる)
token.getToken();
return ResponseEntity.ok().build();
}
}
このエンドポイントは、CSRFトークンを生成し、Cookieとしてクライアントに返すためのものです。
Spring Securityでは、CsrfToken を引数に受け取ることでトークンが生成され、token.getToken() を呼び出すことでCookieへのセットが行われます。
フロントエンドでは、このエンドポイントを初回アクセス時に呼び出すことで、CSRFトークンを取得できます。
まとめ
CSRF対策は、Cookieに保存されたトークンをリクエスト時にヘッダーへ付与することで実現できます。