Django JWT認証の実装|SimpleJWT + Djoser設定とRedisレートリミット

  • DjangoでJWT認証を実装したい
  • SimpleJWTとDjoserをどう組み合わせるか知りたい
  • 不要なエンドポイントを封鎖してセキュリティを高めたい

この記事は、Next.js + Django JWT認証シリーズのバックエンド実装編です。設計の考え方は下記の記事で解説しています。

この記事では、djangorestframework-simplejwtdjoser の設定、Redisを使ったレートリミットミドルウェアの実装を解説します。

必要なパッケージ

JWT認証に必要なパッケージを追加します。

  • djangorestframework-simplejwt
    JWTの発行・検証・リフレッシュ・ブラックリスト管理を担う。
  • djoser
    ユーザー登録・ログイン・パスワードリセットなどの認証エンドポイントを提供するライブラリ。SimpleJWTと組み合わせて使う。
  • django-redis
    RedisをDjangoのキャッシュバックエンドとして使うためのパッケージ。レートリミットのカウンターをマルチワーカー間で共有できる。
  • django-environ
    環境変数を .env ファイルから読み込むユーティリティ。
Bash
# uv を使う場合
uv add djangorestframework djangorestframework-simplejwt djoser django-redis django-environ

# pip を使う場合
pip install djangorestframework djangorestframework-simplejwt djoser django-redis django-environ

INSTALLED_APPS と MIDDLEWARE の設定

インストールしたパッケージを INSTALLED_APPS に追加します。rest_framework_simplejwt.token_blacklist はリフレッシュトークンのローテーション時に古いトークンを無効化するために必要です。

Python
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 を最上位に、カスタムミドルウェアを末尾に追加します。

Python
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など不要な認証方式を残すとセキュリティ上のリスクになるため削除します。

Python
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 の設定

トークンの有効期限とローテーション設定が中心です。各パラメータの意味を確認しておきます。

Python
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 で封鎖することが重要です。

Python
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など複数ワーカーを立てる場合に必要な設定です。

Python
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() を組み合わせることで、レースコンディションが起きないアトミックな実装になります。

Python
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発行・リフレッシュ・ログアウトのエンドポイントを提供します。

Python
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/
    ログイン中のユーザー情報を返す。

バックエンドの設定はこれで完成です。フロントエンド側でこれらのエンドポイントをどう呼び出すかは、次の記事で解説します。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

普段は主にPython開発をしています。
最近はAI駆動開発にも関わっています。

コメント

コメントする