概要
next/headers の cookies() でログイン時に httpOnly Session Cookie を発行し、Server Component・Route Handler・Middleware からそれを読み取って認証状態を確認する。外部ライブラリ不要で Next.js 標準 API のみで完結するセッション管理パターン。
インストール
# 追加インストールは不要
実装
ログイン API(Cookie 発行)
// app/api/login/route.ts
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
const FAKE_USERS: Record<string, string> = {
"user@example.com": "password123",
};
export async function POST(request: Request) {
const body = await request.json() as { email?: string; password?: string };
if (!body.email || !body.password) {
return NextResponse.json({ error: "email and password are required" }, { status: 400 });
}
if (FAKE_USERS[body.email] !== body.password) {
return NextResponse.json({ error: "Invalid credentials" }, { status: 401 });
}
const cookieStore = await cookies();
// セッション ID を発行(実際はランダム生成 + DB保存)
const sessionId = crypto.randomUUID();
cookieStore.set("session_id", sessionId, {
httpOnly: true, // JS から読めないようにする
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
maxAge: 60 * 60 * 24, // 24時間
});
return NextResponse.json({ message: "Logged in" });
}
ログアウト API(Cookie 削除)
// app/api/logout/route.ts
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
export async function POST() {
const cookieStore = await cookies();
cookieStore.delete("session_id");
return NextResponse.json({ message: "Logged out" });
}
Server Component での認証確認
// app/dashboard/page.tsx
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const cookieStore = await cookies();
const sessionId = cookieStore.get("session_id");
if (!sessionId) {
redirect("/login");
}
return (
<main className="p-8">
<h1 className="text-2xl font-bold">ダッシュボード</h1>
<p className="mt-2 text-sm text-gray-600">
セッション ID: {sessionId.value}
</p>
</main>
);
}
Middleware でのルート保護
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const sessionId = request.cookies.get("session_id");
// 未認証の場合はログインページにリダイレクト
if (!sessionId) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/profile/:path*"],
};
クライアント側ログインフォーム
// app/login/page.tsx
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function LoginPage() {
const [error, setError] = useState<string | null>(null);
const router = useRouter();
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 {
const data = await res.json();
setError(data.error ?? "ログインに失敗しました");
}
}
return (
<main className="flex min-h-screen items-center justify-center">
<form onSubmit={handleSubmit} className="w-80 space-y-4">
<h1 className="text-xl font-bold">ログイン</h1>
<input name="email" type="email" placeholder="メールアドレス"
className="block w-full rounded border px-3 py-2 text-sm" />
<input name="password" type="password" placeholder="パスワード"
className="block w-full rounded border px-3 py-2 text-sm" />
{error && <p className="text-sm text-red-500">{error}</p>}
<button type="submit"
className="w-full rounded bg-blue-600 py-2 text-sm text-white hover:bg-blue-700">
ログイン
</button>
</form>
</main>
);
}
ポイント
cookies()は Server Component / Route Handler ではawaitが必要(Next.js 15 から非同期)。Middleware ではrequest.cookiesを直接参照するhttpOnly: trueを設定すると JavaScript(document.cookie)から読めなくなり XSS によるセッション奪取を防げるsecure: trueは HTTPS 接続のみ Cookie を送信する。本番環境では必須- セッション ID は
crypto.randomUUID()で生成しているが、実際のアプリでは DB にセッション情報を保存して ID と紐付けることでセッション無効化(ログアウト)を確実にする nextjs-middleware-authとの使い分け: Middleware での保護ロジックの書き方は同記事を参照。これは Cookie の発行・削除・Server Component での読み取りに焦点を当てているFAKE_USERSの平文パスワード比較はサンプル用。実際のアプリではbcrypt等でハッシュ化したパスワードを DB に保存し、bcrypt.compare()で検証する