Next.js(フロントエンド)と Django + DRF(バックエンド)を連携させ、ユーザ登録、ログイン、ログアウト、メールアクティベーション、パスワードリセットなどを Djoser でまかなう構成を解説します。
ここでは、JWT 認証(rest_framework_simplejwt
)を使うパターンを基本線とします。
全体の流れとしては、バックエンド側で Djoser を設定し、Next.js 側で JWT を取得・保存・更新する仕組みを実装します。
CORS やクッキー管理、トークンリフレッシュ、ログアウトや保護ルートの制御といった部分も含めて確認していきます。
以降では、ファイル構成例も交えつつ「バックエンド設定 → Djoser 設定 → Next.js 側での連携 → 運用上の注意点」まで段階的に解説します。
バックエンド(Django + DRF 側)の準備
プロジェクト作成と環境構築
まずは Django プロジェクトを作成し、仮想環境を構成します。例は次のとおりです。
python -m venv venv
source venv/bin/activate
pip install django djangorestframework djoser djangorestframework-simplejwt django-cors-headers
django-admin startproject backend .
cd backend
python manage.py startapp accounts
この accounts
アプリを認証関連ロジックの拠点とします。
プロジェクト構成例は次のとおりです。
backend/
backend/ ← Django プロジェクトフォルダ
settings.py
urls.py
…
accounts/
models.py
serializers.py
urls.py
views.py
manage.py
settings.py の設定
backend/backend/settings.py
に以下の設定を加えます。
# settings.py(主要部分)
INSTALLED_APPS = [
# Django 標準
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# サードパーティ
"rest_framework",
"rest_framework_simplejwt.token_blacklist",
"djoser",
"corsheaders",
# 自作
"accounts",
]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.common.CommonMiddleware",
]
# CORS 設定(開発時の例)
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
# 本番ドメインも追加します
]
# DRF 設定
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
"DEFAULT_PERMISSION_CLASSES": (
"rest_framework.permissions.IsAuthenticated",
),
}
# Simple JWT 設定
from datetime import timedelta
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=5),
"REFRESH_TOKEN_LIFETIME": timedelta(days=7),
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
"AUTH_HEADER_TYPES": ("Bearer",),
}
# Djoser 設定
DJOSER = {
"LOGIN_FIELD": "email",
"USER_ID_FIELD": "id",
"USER_CREATE_PASSWORD_RETYPE": True,
"SERIALIZERS": {
"user_create": "accounts.serializers.UserCreateSerializer",
"user": "accounts.serializers.UserSerializer",
"current_user": "accounts.serializers.UserSerializer",
},
"TOKEN_MODEL": None, # Token 認証を使わない場合は None
"PASSWORD_RESET_CONFIRM_URL": "auth/reset-password-confirm/?uid={uid}&token={token}",
"ACTIVATION_URL": "auth/activate/?uid={uid}&token={token}",
"SEND_ACTIVATION_EMAIL": True,
"PASSWORD_RESET_SHOW_EMAIL_NOT_FOUND": True,
}
# メールバックエンド(開発時)
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
ROTATE_REFRESH_TOKENS
を有効にするとリフレッシュ時にトークンがローテーションされ、BLACKLIST_AFTER_ROTATION
により古いリフレッシュトークンを無効化できます。ユーザ登録時に追加情報を扱いたい場合は SERIALIZERS
でシリアライザを差し替えます。
accounts/serializers.py(カスタムユーザモデル拡張想定)
email ログインを使いたい場合は AbstractUser
を拡張して email 必須のモデルにすることが多いです。シリアライザの例は次のとおりです。
# accounts/serializers.py
from django.contrib.auth import get_user_model
from djoser.serializers import UserCreateSerializer as DjoserUserCreateSerializer
from djoser.serializers import UserSerializer as DjoserUserSerializer
User = get_user_model()
class UserCreateSerializer(DjoserUserCreateSerializer):
class Meta(DjoserUserCreateSerializer.Meta):
model = User
fields = ("id", "email", "password", "first_name", "last_name")
class UserSerializer(DjoserUserSerializer):
class Meta(DjoserUserSerializer.Meta):
model = User
fields = ("id", "email", "first_name", "last_name")
accounts/models.py(カスタムユーザモデルの例)
# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
# email を識別子にする場合は username の扱いを再設計します
pass
settings.py
に次を追加します。
AUTH_USER_MODEL = "accounts.User"
urls.py に Djoser のエンドポイントを登録
backend/backend/urls.py
を次のように記述します。
# backend/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("auth/", include("djoser.urls")),
path("auth/", include("djoser.urls.jwt")),
]
これで以下の標準エンドポイントが利用できます。
/auth/users/
(登録・取得・更新)/auth/users/me/
(現在のユーザ情報)/auth/users/activation/
,/auth/users/resend_activation/
/auth/users/reset_password/
,/auth/users/reset_password_confirm/
/auth/jwt/create/
(ログイン・アクセストークン取得)/auth/jwt/refresh/
(リフレッシュ)/auth/jwt/verify/
(検証)
マイグレーションとスーパーユーザ作成
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
ここまででバックエンド側は認証機能を提供できる状態になります。
Next.js 側との接続:JWT 管理と API 呼び出し
Next.js 側では、Djoser の JWT エンドポイントで取得したアクセストークンとリフレッシュトークンを管理し、API 呼び出し時に適切なヘッダーを付与します。
以下では App Router(React Server Components)構成を想定した実装例を示します。
HTTP ラッパ(src/lib/api.ts
)
// src/lib/api.ts
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL; // 例: http://localhost:8000
async function fetchWithAuth(
input: RequestInfo,
init?: RequestInit
): Promise<any> {
const cookieStore = cookies();
const accessToken = cookieStore.get("accessToken")?.value;
const headers = new Headers(init?.headers);
headers.set("Content-Type", "application/json");
if (accessToken) {
headers.set("Authorization", `Bearer ${accessToken}`);
}
const resp = await fetch(`${BASE_URL}${input}`, {
credentials: "include",
...init,
headers,
});
if (resp.status === 401) {
// アクセストークン失効時はリフレッシュを試みます
const refreshResp = await fetch(`${BASE_URL}/auth/jwt/refresh/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh: cookieStore.get("refreshToken")?.value }),
});
if (refreshResp.ok) {
const data = await refreshResp.json();
headers.set("Authorization", `Bearer ${data.access}`);
const retry = await fetch(`${BASE_URL}${input}`, {
credentials: "include",
...init,
headers,
});
return retry.json();
} else {
redirect("/login");
}
}
return resp.json();
}
export default fetchWithAuth;
認証ユーティリティ(サーバーアクション)
// src/app/auth/actions.ts
"use server";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
const BASE = process.env.NEXT_PUBLIC_API_BASE_URL;
export async function loginUser(email: string, password: string) {
const resp = await fetch(`${BASE}/auth/jwt/create/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (!resp.ok) {
throw new Error("ログインに失敗しました");
}
const data = await resp.json();
const cookieStore = cookies();
cookieStore.set("accessToken", data.access, { path: "/", httpOnly: true });
cookieStore.set("refreshToken", data.refresh, { path: "/", httpOnly: true });
redirect("/");
}
export function logoutUser() {
const cookieStore = cookies();
cookieStore.delete("accessToken");
cookieStore.delete("refreshToken");
redirect("/login");
}
ログインページ(クライアントコンポーネント)
// app/login/page.tsx
"use client";
import { useState } from "react";
import { loginUser } from "../auth/actions";
export default function LoginPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await loginUser(email, password);
} catch (err: any) {
setError(err.message || "ログイン中にエラーが発生しました");
}
};
return (
<main>
<h1>ログイン</h1>
<form onSubmit={onSubmit}>
<label>
Email:
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
</label>
<label>
パスワード:
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
</label>
{error && <p style={{ color: "red" }}>{error}</p>}
<button type="submit">ログイン</button>
</form>
</main>
);
}
プロテクトルートの例(サーバーコンポーネント)
// app/protected/page.tsx
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import fetchWithAuth from "@/lib/api";
export default async function ProtectedPage() {
const cookieStore = cookies();
const accessToken = cookieStore.get("accessToken")?.value;
if (!accessToken) {
redirect("/login");
}
const user = await fetchWithAuth("/auth/users/me/", { method: "GET" });
return (
<main>
<h1>ようこそ、{user.email} さん</h1>
</main>
);
}
運用上の注意とカスタマイズ
メールアクティベーション
SEND_ACTIVATION_EMAIL = True
により、登録時に確認メールを送信できます。
開発環境ではコンソールバックエンドを使って挙動を確認し、本番環境では SMTP を設定します。
パスワードリセット
パスワードリセットは /auth/users/reset_password/
にメールアドレスを送信し、
メールに含まれる UID・トークンを用いて /auth/users/reset_password_confirm/
で
新しいパスワードを設定します。
Next.js 側ではこれに対応するフォーム画面を用意します。
トークン無効化とログアウト
リフレッシュトークンのローテーションとブラックリスト化を併用し、トークン再利用を防ぎます。
必要に応じて /auth/jwt/blacklist/
を利用します。
Next.js 側では logoutUser()
を呼び出してクッキーを削除し、安全にログアウトします。
クッキー設定
クッキーには httpOnly
・Secure
・SameSite
属性を設定します。
これにより、XSSやセッション固定攻撃を防ぐことができます。
CORS と CSRF
django-cors-headers
で Next.js のオリジンを明示的に許可します。
JWT 認証を使う場合、通常 CSRF トークンは不要ですが、フォームベースの POST では CSRF トークンを利用する設計も可能です。
Djoser のカスタマイズ
Djoser のデフォルトシリアライザやビューは拡張可能です。
ユーザ登録時に追加フィールドを返したり、ログイン後に独自のレスポンスを返すなど、柔軟な実装ができます。
認証方式の比較
JWT 認証と rest_framework.authtoken
のいずれも利用可能です。
ただし、有効期限管理やスケーラビリティの観点からは JWT を推奨します。
まとめ(チェックポイント)
以下が一通り動作していれば、Next.js と DRF をつなぐ安全な JWT 認証基盤が整います。
/auth/jwt/create/
でログイントークンを取得できる/auth/jwt/refresh/
でリフレッシュが行える/auth/users/
でユーザ登録ができる/auth/users/me/
でユーザ情報を取得できる- メールアクティベーションとパスワードリセットが機能する
- Next.js 側でトークンの保存・更新・削除が正しく行える
- 保護ルートへのアクセス制御が機能する
- CORS・クッキー・(必要なら)CSRF の設定が適切である
今後は、Auth.js(NextAuth.js)との比較や統合、型定義の最適化、テスト観点の追加なども掘り下げていきます。
コメント