- DjangoでJWT認証を実装したい
- SimpleJWTとDjoserをどう組み合わせるか知りたい
- 不要なエンドポイントを封鎖してセキュリティを高めたい
この記事は、Next.js + Django JWT認証シリーズのバックエンド実装編です。設計の考え方は下記の記事で解説しています。

この記事では、djangorestframework-simplejwt と djoser の設定、Redisを使ったレートリミットミドルウェアの実装を解説します。
必要なパッケージ
JWT認証に必要なパッケージを追加します。
- djangorestframework-simplejwt
JWTの発行・検証・リフレッシュ・ブラックリスト管理を担う。 - djoser
ユーザー登録・ログイン・パスワードリセットなどの認証エンドポイントを提供するライブラリ。SimpleJWTと組み合わせて使う。 - django-redis
RedisをDjangoのキャッシュバックエンドとして使うためのパッケージ。レートリミットのカウンターをマルチワーカー間で共有できる。 - django-environ
環境変数を.envファイルから読み込むユーティリティ。
# uv を使う場合
uv add djangorestframework djangorestframework-simplejwt djoser django-redis django-environ
# pip を使う場合
pip install djangorestframework djangorestframework-simplejwt djoser django-redis django-environINSTALLED_APPS と MIDDLEWARE の設定
インストールしたパッケージを INSTALLED_APPS に追加します。rest_framework_simplejwt.token_blacklist はリフレッシュトークンのローテーション時に古いトークンを無効化するために必要です。
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# サードパーティ
"corsheaders",
"rest_framework",
"rest_framework_simplejwt",
"rest_framework_simplejwt.token_blacklist",
"djoser",
# ローカルアプリ
"apps.users",
]MIDDLEWARE には CorsMiddleware を最上位に、カスタムミドルウェアを末尾に追加します。
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware", # 最上位に置く
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"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.clickjacking.XFrameOptionsMiddleware",
# カスタム
"apps.users.middleware.RateLimitMiddleware",
"apps.users.middleware.SecurityHeadersMiddleware",
]REST_FRAMEWORK の設定
デフォルトの認証クラスを JWTAuthentication のみにします。Sessionなど不要な認証方式を残すとセキュリティ上のリスクになるため削除します。
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
"DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer",
],
}SimpleJWT の設定
トークンの有効期限とローテーション設定が中心です。各パラメータの意味を確認しておきます。
from datetime import timedelta
SIMPLE_JWT = {
# アクセストークン: 15分(短命にしてリスクを限定)
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
# リフレッシュトークン: 14日
"REFRESH_TOKEN_LIFETIME": timedelta(days=14),
# リフレッシュのたびに新しいリフレッシュトークンを発行
"ROTATE_REFRESH_TOKENS": True,
# 使用済みリフレッシュトークンをブラックリストに登録
"BLACKLIST_AFTER_ROTATION": True,
"AUTH_HEADER_TYPES": ("Bearer",),
"TOKEN_TYPE_CLAIM": "token_type",
}ROTATE_REFRESH_TOKENS = True を設定すると、リフレッシュのたびに新しいリフレッシュトークンが発行されます。古いトークンがブラックリストに入るため、トークン漏洩時の被害を限定できます。
ただし、フロントエンドで複数のAPIリクエストが並走してリフレッシュを呼び出すと競合が起きます。この問題の対策はフロントエンド実装の記事で解説します。
Djoser の設定 – 不要なエンドポイントを封鎖する
Djoserはデフォルトでユーザー登録・削除・メール変更など多くのエンドポイントを公開します。使わないエンドポイントを IsAdminUser で封鎖することが重要です。
DJOSER = {
"SERIALIZERS": {
"current_user": "apps.users.serializers.UserProfileSerializer",
},
"PERMISSIONS": {
"current_user": ["rest_framework.permissions.IsAuthenticated"],
# /auth/users/ への直接登録を禁止(独自の承認フローを使うため)
"user_create": ["rest_framework.permissions.IsAdminUser"],
# メールアドレス変更を禁止(乗っ取り時に元オーナーが復旧できなくなるため)
"set_username": ["rest_framework.permissions.IsAdminUser"],
# 自己削除を禁止(関連データが失われるため)
"user_delete": ["rest_framework.permissions.IsAdminUser"],
# djoser標準のパスワードリセットを封鎖(独自実装を使うため)
"password_reset": ["rest_framework.permissions.IsAdminUser"],
"password_reset_confirm": ["rest_framework.permissions.IsAdminUser"],
},
# ユーザー列挙攻撃を防ぐ(未登録メールでも同じレスポンスを返す)
"PASSWORD_RESET_SHOW_EMAIL_NOT_FOUND": False,
"USERNAME_RESET_SHOW_EMAIL_NOT_FOUND": False,
}特に PASSWORD_RESET_SHOW_EMAIL_NOT_FOUND = False は重要です。これを有効にしないと、未登録のメールアドレスを送信したときに「そのメールは登録されていません」というレスポンスが返り、攻撃者にユーザー一覧を推測させてしまいます。
Redis キャッシュの設定
レートリミットのカウンターをマルチワーカー間で共有するために、Redisをキャッシュバックエンドとして使います。Gunicornなど複数ワーカーを立てる場合に必要な設定です。
import environ
env = environ.Env()
REDIS_URL = env("REDIS_URL", default="redis://localhost:6379/1")
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": REDIS_URL,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
}
}RateLimitMiddleware の実装
Redisキャッシュを使ってIPアドレスごとにリクエスト数を制限するカスタムミドルウェアを実装します。cache.add() と cache.incr() を組み合わせることで、レースコンディションが起きないアトミックな実装になります。
import json
from django.conf import settings
from django.core.cache import cache
from django.http import HttpResponse
from django.utils.deprecation import MiddlewareMixin
class RateLimitMiddleware(MiddlewareMixin):
def __init__(self, get_response):
super().__init__(get_response)
auth_req = 120 if settings.DEBUG else 30
self.rate_limits = {
"default": {"requests": 100, "window": 60},
"auth": {"requests": auth_req, "window": 60},
"register": {"requests": 5, "window": 3600},
"contact": {"requests": 5, "window": 3600},
}
def process_request(self, request):
exempt = ["/static/", "/media/", "/favicon.ico", "/admin/"]
if any(request.path.startswith(p) for p in exempt):
return None
ip = self._get_client_ip(request)
endpoint = self._get_endpoint_type(request.path)
if not self._check_rate_limit(ip, endpoint):
return HttpResponse(
json.dumps({"detail": "Rate limit exceeded."}),
content_type="application/json",
status=429,
)
return None
def _get_client_ip(self, request):
"""リバースプロキシ経由の実クライアントIPを取得する"""
remote_addr = request.META.get("REMOTE_ADDR", "")
trusted = getattr(settings, "TRUSTED_PROXIES", [])
if remote_addr in trusted:
xff = request.META.get("HTTP_X_FORWARDED_FOR", "")
if xff:
# nginx は実クライアントIPを末尾に追記する
return xff.split(",")[-1].strip()
return remote_addr
def _get_endpoint_type(self, path):
if "/api/auth/register/" in path:
return "register"
if "/auth/" in path or "/login" in path:
return "auth"
if "/api/contact/" in path:
return "contact"
return "default"
def _check_rate_limit(self, ip, endpoint):
"""アトミックなカウントアップで競合を防ぐ"""
config = self.rate_limits.get(endpoint, self.rate_limits["default"])
key = f"rate_limit:{endpoint}:{ip}"
# キーが存在しない場合のみ0で初期化(TTL付き)
cache.add(key, 0, config["window"])
# アトミックにインクリメント
count = cache.incr(key)
return count <= config["requests"]ポイントは cache.add() と cache.incr() の組み合わせです。cache.add() はキーが存在しない場合のみ値をセットし、TTLも同時に設定します。その後 cache.incr() でアトミックにカウントを増やします。
もし cache.get() → cache.set() のパターンで書いてしまうと、並走リクエストが同時に「0件」を読んでそれぞれがカウントをリセットするレースコンディションが起きます。
URL の設定
DjoserのURLを2行追加します。djoser.urls がユーザー管理系エンドポイント、djoser.urls.jwt がJWT発行・リフレッシュ・ログアウトのエンドポイントを提供します。
from django.urls import path, include
urlpatterns = [
path("auth/", include("djoser.urls")),
path("auth/", include("djoser.urls.jwt")),
path("api/", include("apps.main.urls")),
]この設定で有効になる主なエンドポイントです。
- POST /auth/jwt/create/
ログイン。アクセストークンとリフレッシュトークンを返す。 - POST /auth/jwt/refresh/
リフレッシュ。HttpOnly CookieのリフレッシュトークンをもとにアクセストークンをJSON で返す。 - POST /auth/logout/
ログアウト。リフレッシュトークンをブラックリストに登録する。 - GET /auth/users/me/
ログイン中のユーザー情報を返す。
バックエンドの設定はこれで完成です。フロントエンド側でこれらのエンドポイントをどう呼び出すかは、次の記事で解説します。

コメント