失敗の種類と対応手段
認証・権限チェックの失敗は 4 種類に分けて考えます。
| 失敗の種類 | 対応手段 | 理由 |
|---|---|---|
| 未認証(ログインしていない) | redirect('/login') | ログインすれば解決できる |
| 権限不足・存在を隠したい | notFound() | リソースの存在自体を知らせない |
| 権限不足・明示したい | redirect('/forbidden') または専用ページ | 「見られないこと」をユーザーに伝える |
| フォーム送信での認証失敗 | return { formError } | フォームに回復手段を提供する |
| DB 障害・想定外エラー | throw(伝播) | error.tsx にキャッチさせる |
未認証 → redirect()
セッションがない場合はログインページへ転送します。ログインすれば目的のページに到達できるため、redirect() が自然です。
// Server Component: セッション確認
import { redirect } from "next/navigation";
import { cookies } from "next/headers";
export default async function DashboardPage() {
const cookieStore = await cookies();
if (!cookieStore.get("session_id")) {
redirect("/login"); // ログインすれば解決できる
}
return <Dashboard />;
}
ログイン後に元のページへ戻れるようにするには、redirect 先に next パラメータを付けるパターンがあります。
redirect(`/login?next=${encodeURIComponent(request.nextUrl.pathname)}`);
権限不足 — notFound() で存在を隠す
別ユーザーのリソースに不正アクセスされた場合、403 Forbidden を返すとリソースが存在することが分かります。セキュリティ上、存在自体を隠したい場合は notFound() が適切です。
// 他人の投稿にアクセスしようとした場合
export default async function PostPage({ params }: { params: { id: string } }) {
const session = await getSession();
const post = await db.post.findUnique({ where: { id: params.id } });
if (!post) {
notFound(); // 存在しない
}
if (post.authorId !== session?.userId) {
notFound(); // 存在するが見せない(403 より 404 が安全)
}
return <PostDetail post={post} />;
}
notFound() を使う場面:
- 他人のプライベートリソース(DM・下書き・注文履歴など)
- 存在確認を許可したくない API エンドポイント
notFound() を使わない場面:
- 管理者権限が必要なページ(権限不足であることをユーザーに伝えるべき)
- RBAC で明示的なエラーメッセージが必要な場面
権限不足 — 明示する場合
管理者専用ページなど、「ログインはしているが権限がない」ことをユーザーに伝えたい場合は専用ページへ転送します。
import { redirect } from "next/navigation";
export default async function AdminPage() {
const session = await getSession();
if (!session) {
redirect("/login"); // 未認証
}
if (session.role !== "admin") {
redirect("/forbidden"); // 権限不足(403 相当)
}
return <AdminDashboard />;
}
// app/forbidden/page.tsx
export default function ForbiddenPage() {
return (
<div>
<h1>アクセス権限がありません</h1>
<p>このページにアクセスする権限がありません。</p>
</div>
);
}
フォーム送信での認証失敗 → return
ログインフォームでパスワードが違う場合など、フォームにエラーメッセージを返す場合は return { formError } を使います。redirect() や throw を使うとフォームの入力値が失われたり、ページ全体がエラー画面になります。
"use server";
export async function loginAction(
_prev: { formError?: string } | null,
formData: FormData
) {
const email = formData.get("email") as string;
const password = formData.get("password") as string;
// バリデーション
if (!email || !password) {
return { formError: "メールアドレスとパスワードを入力してください" };
}
// 認証失敗(期待される失敗)
const user = await authenticate(email, password);
if (!user) {
return { formError: "メールアドレスまたはパスワードが正しくありません" };
}
// 成功 → リダイレクト(try/catch の外)
await createSession(user.id);
redirect("/dashboard");
}
Middleware / Layout / Server Component での責務分離
| 場所 | 向いているチェック | 制約 |
|---|---|---|
| Middleware | 全ルートの一括保護(未認証ガード) | Edge Runtime のみ・DB 直接参照不可 |
| Layout | ルートグループ単位の保護 | 一度キャッシュされると再実行されないケースあり |
| Server Component | リソース単位の細かい権限チェック | ページごとに実装が必要 |
重複実装は避け、責務を決めて 1 箇所で管理することを推奨します。Middleware で未認証をはじき、個別の権限チェックは Server Component で行うパターンが一般的です。
判断フロー
アクセス失敗
├── セッションがない(未認証)
│ └── redirect('/login')
├── セッションはあるが権限がない
│ ├── 存在を隠したい → notFound()
│ └── 権限不足を伝えたい → redirect('/forbidden') または forbidden()
├── フォーム送信での認証失敗
│ └── return { formError } → useActionState でフォームに表示
└── DB 障害・想定外エラー
└── throw → error.tsx がキャッチ
認証方式(Middleware / Layout / Server Component どこで判定するか)→ Next.js 認証方式比較
失敗の種類と扱い方の基本 → エラーハンドリングの考え方
Session 取得関数の型設計と責務分離 → 型安全な Session 管理パターン