概要
cookies() でセッション Cookie を確認し、redirect() で条件分岐するパターン。未認証ユーザーが保護ページにアクセスしたときログインページへ、ログイン済みユーザーがログインページにアクセスしたときダッシュボードへ、それぞれ redirect する。
インストール
# 追加インストールは不要
実装
認証済みユーザーのみアクセス可能なページ
// 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="mx-auto max-w-2xl p-8">
<h1 className="mb-4 text-2xl font-bold">ダッシュボード</h1>
<p className="text-gray-600">認証済みユーザーのみ表示されます。</p>
</main>
);
}
ログイン済みユーザーはリダイレクト(ログインページ)
// app/login/page.tsx
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import LoginForm from "@/components/LoginForm";
export default async function LoginPage() {
const cookieStore = await cookies();
const sessionId = cookieStore.get("session_id");
// ログイン済みならダッシュボードへ
if (sessionId) {
redirect("/dashboard");
}
return (
<main className="flex min-h-screen items-center justify-center">
<LoginForm />
</main>
);
}
ログインフォーム(Client Component)
// components/LoginForm.tsx
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function LoginForm() {
const router = useRouter();
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setLoading(true);
setError(null);
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");
router.refresh();
} else {
setError("メールアドレスまたはパスワードが正しくありません");
setLoading(false);
}
}
return (
<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"
disabled={loading}
className="w-full rounded bg-blue-600 py-2 text-sm text-white hover:bg-blue-700 disabled:opacity-50"
>
{loading ? "ログイン中…" : "ログイン"}
</button>
</form>
);
}
セッション発行 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: "ログイン成功" });
}
ログアウト API
// 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: "ログアウト成功" });
}
ポイント
- Server Component から直接
redirect()を呼ぶと、コンポーネントのレンダリングを中断して即座にリダイレクトする。try/catchで囲まない限りコンポーネントの後続コードは実行されない - ログインページ側でも
sessionIdを確認してダッシュボードへ redirect することで、ログイン済みユーザーがログインページを再表示するのを防ぐ(二重ログイン防止) redirect()はnext/navigationからインポートする。Server Component / layout / Route Handler から使用できる。Client Component ではuseRouter().push()を使う- ログイン成功後に
router.push("/dashboard")とrouter.refresh()を両方呼ぶ。refresh()でサーバーコンポーネントを再フェッチして Cookie を読み込ませることが重要 - 実際のアプリでは
sessionId.valueを DB と照合してセッションの有効性を確認する。Cookie 値の存在だけで認証済みと判断するのはサンプル簡略化のため