概要
app/(dashboard)/layout.tsx でセッション Cookie を確認し、未認証ユーザーをログインページへ redirect() する。このレイアウト配下に置いたページはすべて自動的に認証保護される。ミドルウェアを使わずに特定ルートグループだけを保護したい場合に有効なパターン。
インストール
# 追加インストールは不要
実装
ディレクトリ構成
app/
├── (public)/
│ ├── login/
│ │ └── page.tsx # 公開ページ
│ └── layout.tsx
├── (dashboard)/
│ ├── layout.tsx # ← ここで認証チェック
│ ├── page.tsx # /dashboard
│ ├── settings/
│ │ └── page.tsx # /settings(自動保護)
│ └── profile/
│ └── page.tsx # /profile(自動保護)
└── layout.tsx # ルートレイアウト
保護レイアウト(認証チェック)
// app/(dashboard)/layout.tsx
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import type { ReactNode } from "react";
type Props = {
children: ReactNode;
};
export default async function DashboardLayout({ children }: Props) {
const cookieStore = await cookies();
const sessionId = cookieStore.get("session_id");
// セッションがなければログインページへ
if (!sessionId) {
redirect("/login");
}
return (
<div className="flex min-h-screen">
{/* サイドバー */}
<aside className="w-60 border-r bg-gray-50 p-6">
<nav className="space-y-2">
<a href="/dashboard" className="block rounded px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100">
ダッシュボード
</a>
<a href="/settings" className="block rounded px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100">
設定
</a>
<a href="/profile" className="block rounded px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100">
プロフィール
</a>
</nav>
</aside>
{/* メインコンテンツ */}
<main className="flex-1 p-8">{children}</main>
</div>
);
}
保護対象ページ(認証チェック不要)
// app/(dashboard)/page.tsx
export default function DashboardPage() {
return (
<div>
<h1 className="mb-4 text-2xl font-bold">ダッシュボード</h1>
<p className="text-gray-600">認証済みユーザーのみ表示されます。</p>
</div>
);
}
公開ページ(ログインフォーム)
// app/(public)/login/page.tsx
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function LoginPage() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const fd = new FormData(e.currentTarget);
const res = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: fd.get("email"),
password: fd.get("password"),
}),
});
if (res.ok) {
router.push("/dashboard");
} else {
setError("メールアドレスまたはパスワードが正しくありません");
}
}
return (
<main className="flex min-h-screen items-center justify-center">
<form onSubmit={handleSubmit} className="w-80 space-y-4 rounded-lg border p-8">
<h1 className="text-xl font-bold">ログイン</h1>
{error && <p className="text-sm text-red-500">{error}</p>}
<input
name="email"
type="email"
placeholder="メールアドレス"
required
className="block w-full rounded border px-3 py-2 text-sm"
/>
<input
name="password"
type="password"
placeholder="パスワード"
required
className="block w-full rounded border px-3 py-2 text-sm"
/>
<button
type="submit"
className="w-full rounded bg-blue-600 py-2 text-sm text-white hover:bg-blue-700"
>
ログイン
</button>
</form>
</main>
);
}
セッション発行 API
// app/api/login/route.ts
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const body = await request.json() as { email: string; password: string };
// 実際のアプリでは DB + bcrypt.compare() で検証する
if (body.email !== "user@example.com" || body.password !== "password") {
return NextResponse.json({ error: "認証失敗" }, { status: 401 });
}
const cookieStore = await cookies();
cookieStore.set("session_id", crypto.randomUUID(), {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
maxAge: 60 * 60 * 24,
});
return NextResponse.json({ message: "ログイン成功" });
}
ポイント
app/(dashboard)/layout.tsxは Route Group(dashboard)の共通レイアウト。cookies()で Cookie を確認し、未認証ならredirect("/login")する。このレイアウトの配下に追加したページはすべて自動的に保護される- Route Group(括弧付きディレクトリ)は URL パスに影響しないため、
(dashboard)配下のページも/dashboard・/settingsのように通常の URL でアクセスできる nextjs-middleware-authとの違い: Middleware は全リクエストに適用されるため柔軟だが設定が複雑。layout での保護は特定のルートグループのみを対象にできてシンプル- 実際のアプリでは
sessionId.valueを DB と照合してセッションの有効性を確認する。Cookie 値が存在するだけで認証済みと判断するのはサンプル簡略化のため redirect()はnext/navigationからインポートする。Server Component / layout から使える。Client Component ではuseRouter().push()を使う