はじめに
はじめまして!サイバーセキュリティ(ゼロトラストやWebAuthn)を研究している情報科学専攻の大学生です。
現在「セキュア社内掲示板」を開発する7日間のプロジェクトに取り組んでいます。
今回はDay 5の総決算として、完成したFastAPIバックエンドと、新規構築したNext.jsフロントエンドを結合し、セキュアなログイン通信を実装した際の手順と、「環境構築・API連携の罠」についてまとめました。
1. Next.js × Tailwind CSSで作るサイバー風UI
フロントエンドは Next.js (App Router) を採用し、スタイリングには Tailwind CSS を使用しました。ゼロトラストをテーマにしたシステムということで、ダークテーマを基調とし、ネオンカラーとグラスモーフィズム(ガラスのような透過エフェクト)を取り入れたUIを作成しています。
Tailwind CSSのユーティリティクラスを使うと、CSSファイルに複雑な記述をしなくても、直感的にリッチなデザインが作れます。
// ログインパネルのグラスモーフィズムとネオンエフェクト実装例
<div className="p-10 rounded-2xl bg-gray-900/50 backdrop-blur-md border border-cyan-500/30 shadow-[0_0_40px_rgba(6,182,212,0.15)]">
<h2 className="text-2xl font-bold text-cyan-400 tracking-[0.2em] drop-shadow-[0_0_8px_rgba(6,182,212,0.8)]">
AUTHENTICATION
</h2>
{/* 入力フォームなどが続く... */}
</div>
2. CORSエラーとSOP(同一オリジンポリシー)の理解
フロントエンド(http://localhost:3000)からバックエンド(http://localhost:8000)へAPIリクエストを送ろうとすると、ブラウザのセキュリティ機構にブロックされます。
- SOP(同一オリジンポリシー): 悪意のある罠サイトから、別サイト(今回でいうAPIサーバー)への不正な通信(CSRF攻撃など)を防ぐためのブラウザの基本ルールです。
- CORS(Cross-Origin Resource Sharing): SOPの制限を正当な理由で回避し、異なるオリジン間の通信を許可する仕組みです。
これを解決するため、FastAPI側でミドルウェアを設定します。今回はJWTをHttpOnly Cookieとしてやり取りするため、allow_credentials=True が必須になります。
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True, # Cookieによる認証情報を含めるために必須
allow_methods=["*"],
allow_headers=["*"],
)
3. Next.jsからのAPIリクエスト(Cookieの送信)
フロントエンドの fetch 処理でも、Cookieを安全に送受信するための設定が必要です。credentials: "include" を指定することで、CORS設定されたバックエンドとの間でセッションを維持できます。
const response = await fetch("http://localhost:8000/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
credentials: "include", // ★超重要: これがないとCookieがブラウザに保存されない
});
4. API結合で立ちふさがった数々の罠と解決策
ここからは、今回の実装で実際に遭遇した強烈なエラーたちとその解決策を共有します。
罠1:環境分離とパスの迷子
バックエンドを起動しようとして uvicorn コマンドを叩くと、なぜかモジュールが見つからないエラーが連発しました。
原因は、プロジェクト固有の仮想環境(.venv)に入ったつもりでも、PC全体のPython環境(pyenv)が優先して呼び出されていたり、フォルダ移動で .venv 内部の絶対パスが壊れていたことでした。
-
解決策: 壊れた
.venvは原因究明に時間をかけず「削除して即再構築」が鉄則です。また、確実な実行のためpython -m uvicorn main:app --reloadのように、現在のPython経由で明示的に起動することで解決しました。
罠2:passlib と bcrypt のバージョン競合バグ
テストユーザーを登録しようとしたところ、FastAPIから 500 Internal Server Error が返ってきました。ログを確認すると、パスワードハッシュ化ライブラリの内部で以下のエラーが発生。
ValueError: password cannot be longer than 72 bytes, truncate manually if necessary
入力したパスワードは短いにもかかわらずこのエラーが出ました。原因は、passlib が最新の bcrypt (v4系) の仕様変更に対応しきれずクラッシュする既知のバグでした。
-
解決策:
pip install bcrypt==3.2.2と、互換性のある安定版へダウングレードすることで無事に解決しました。
罠3:Reactクラッシュ祭(Pydantic 422 Validation Error)
いざログインを実行すると、以下のエラーが出現しました。
Objects are not valid as a React child (found: object with keys {type, loc, msg, input}).
FastAPIはデータ検証(Pydantic)に失敗すると、親切に「どこがどう間違っているか」を辞書(オブジェクト)形式の422エラーで返してくれます。しかし、Reactはオブジェクトをそのまま画面(HTML)に表示しようとするとクラッシュしてしまいます。
今回は「バックエンドはJSONを待っているのに、フロントエンドがFormDataで送っていた」ことが原因でした。
- 解決策: 送信形式をJSONに統一し、エラーハンドリング側で「オブジェクトが返ってきたら文字列のメッセージに変換する」安全対策を実装しました。
const errorData = await response.json();
if (typeof errorData.detail === "string") {
setErrorMsg(errorData.detail); // 文字列ならそのまま表示
} else if (Array.isArray(errorData.detail)) {
setErrorMsg("入力形式が正しくありません (Validation Error)"); // Pydanticエラーの場合
}
おわりに
フロントエンドとバックエンドを繋ぐフェーズは、データの形式、ネットワークの設定、ブラウザのセキュリティ仕様が複雑に絡み合い、エラーの宝庫となります。しかし、エラーログをしっかり読み解き、一つひとつ切り分けて解決していくプロセスこそが勉強になると実感しました。
次回(Day 6)は、いよいよゼロトラストの要となる「WebAuthn(FIDO2)」のフロントエンド実装に挑戦します!