Django DRF #2 認証 / 権限 — Token、JWT、custom permission

読了 8分

#1 で作った PostViewSet は誰でも記事を書いて修正できる状態です。本物のサービスにするには 2 つの問いに答える必要があります。

  • 認証 (Authentication) — 「あなたは誰か?」
  • 認可 (Authorization) — 「その人がこの仕事をしてよいか?」

DRF は 2 か所にそれぞれ 認証クラス (authentication classes)権限クラス (permission classes) を持ちます。両者は完全に分離した概念ですが、一緒に動作します。

DRF 認証 vs Django 本体の認証 #

中級 #4 ユーザー / 権限 の Django 本体認証と DRF 認証は位置づけが違います。

Django 本体認証DRF 認証
主な用途サーバーレンダリングのページJSON API
保存場所session + cookieheader (Token / JWT)
ミドルウェアAuthenticationMiddlewareDEFAULT_AUTHENTICATION_CLASSES
ログイン formLoginView (template)obtain_auth_token / JWT view
request.userミドルウェアが埋める認証クラスが埋める
一緒に使えるかはい (SessionAuth も DRF がサポート)

同じ User モデルを共有し、Django の is_authenticatedpermissions のようなものもそのまま使います。認証のトランスポート (どこから資格を取ってくるか) だけが違います。

DRF の認証クラス #

DRF は 1 つのリクエストに 複数の認証クラスを順に試行 します。最初に成功した認証を採択します。

settings.py — グローバル設定
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.SessionAuthentication",
        "rest_framework.authentication.TokenAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
}

デフォルトの設定はグローバル。View ごとにオーバーライド可能。

よく使う認証クラス #

クラス適用先
SessionAuthentication同一 origin のブラウザ (Browsable API、admin と一緒に使うとき)
BasicAuthenticationデバッグ / テスト (プロダクション非推奨)
TokenAuthenticationシンプルなトークン。DB の authtoken_token に保存
JWT (simplejwt)標準トークン。stateless、有効期限 / refresh
OAuth2外部 IdP、複数クライアント (django-oauth-toolkit)

小さなサービス / モバイルアプリは普通 JWT (simplejwt) が最初の選択。非常にシンプルな内部ツールなら TokenAuthentication でも十分です。

TokenAuthentication で始める #

最も簡単なトークンの流れ。Django の authtoken アプリが token テーブルを作り、obtain_auth_token view がメール / パスワードでトークンを発行してくれます。

settings.py
INSTALLED_APPS = [
    ...,
    "rest_framework",
    "rest_framework.authtoken",   # 追加
]

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.TokenAuthentication",
        "rest_framework.authentication.SessionAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
}
マイグレーション + ユーザー別トークンの自動生成シグナル
uv run python manage.py migrate
blog/signals.py — 会員登録時にトークン自動生成
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token


@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    if created:
        Token.objects.create(user=instance)

中級 #3 Signals のパターンそのまま。ユーザーが生成される瞬間にトークンも一緒に。

トークン発行 view #

mysite/urls.py
from rest_framework.authtoken.views import obtain_auth_token

urlpatterns = [
    ...,
    path("api/auth/token/", obtain_auth_token),
]
リクエスト
curl -X POST http://localhost:8000/api/auth/token/ \
  -d "username=alice&password=secret"
レスポンス
{"token": "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"}

以後、すべてのリクエストにヘッダでトークンを付けます。

認証されたリクエスト
curl http://localhost:8000/api/posts/ \
  -H "Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"

Token のプレフィックスが核心 — JWT の Bearer とは違います。

TokenAuthentication の限界 #

  • 有効期限なし — トークンが永久。失効させるには DB から削除しなければなりません。
  • stateful — 検証のたびに DB クエリ。
  • refresh メカニズムなし

小さな内部ツールやプロトタイプには十分ですが、一般のユーザー向けサービスには JWT のほうが向きます。

JWT — djangorestframework-simplejwt #

JWT は stateless、有効期限 / refresh が標準 — FastAPI #4 認証 で扱ったその JWT と同じ概念です。DRF 陣営の標準ライブラリは simplejwt

インストール
uv add djangorestframework-simplejwt
settings.py
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
        "rest_framework.authentication.SessionAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
}

from datetime import timedelta

SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=30),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=7),
    "ROTATE_REFRESH_TOKENS": True,
    "BLACKLIST_AFTER_ROTATION": True,
    "AUTH_HEADER_TYPES": ("Bearer",),
}

ROTATE_REFRESH_TOKENS + BLACKLIST_AFTER_ROTATION の組み合わせが推奨 — refresh のたびに新しい refresh トークンを渡し、古いものはブラックリストへ。奪取されたトークンの寿命を短く保ちます (ブラックリストは INSTALLED_APPSrest_framework_simplejwt.token_blacklist を追加)。

mysite/urls.py
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
    TokenVerifyView,
)

urlpatterns = [
    ...,
    path("api/auth/token/", TokenObtainPairView.as_view()),
    path("api/auth/token/refresh/", TokenRefreshView.as_view()),
    path("api/auth/token/verify/", TokenVerifyView.as_view()),
]

発行の流れ #

ログイン — access + refresh を一度に
curl -X POST http://localhost:8000/api/auth/token/ \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"secret"}'
レスポンス
{
  "access": "eyJhbGciOi...",
  "refresh": "eyJhbGciOi..."
}
API 呼び出し — Bearer プレフィックス
curl http://localhost:8000/api/posts/ \
  -H "Authorization: Bearer eyJhbGciOi..."

Refresh #

Access が失効したら (30 分後)、refresh トークンで新しい access を受け取ります。

refresh
curl -X POST http://localhost:8000/api/auth/token/refresh/ \
  -H "Content-Type: application/json" \
  -d '{"refresh":"eyJhbGciOi..."}'
{"access": "eyJhbGciOi... 新しいトークン..."}

TokenAuthentication vs JWT #

TokenAuthJWT (simplejwt)
保存場所DBクライアントのみ
検証コストDB クエリ署名検証 (メモリ)
有効期限なし (手動削除)自動 (exp)
Refreshなしあり
失効トークン削除 → 即時期限まで、またはブラックリスト
ヘッダ prefixTokenBearer
向く場面内部ツール、シンプル一般ユーザー向けサービス

Permission クラス #

認証が終わったら (request.user が埋まった後)、その人がこの動作をしてよいか? を検査するのが permission。

ビルトインの permission クラス #

クラス意味
AllowAny誰でも (認証しなくてよい)
IsAuthenticatedログインしたユーザーのみ
IsAdminUseris_staff=True のみ
IsAuthenticatedOrReadOnly読み取りは誰でも、書き込みはログインユーザーのみ
DjangoModelPermissionsDjango の add_change_delete_ 権限 (中級 #4)
DjangoObjectPermissionsオブジェクト単位の権限 (django-guardian と一緒に)

View 別オーバーライド #

blog/views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from .models import Post
from .serializers import PostSerializer


class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [IsAuthenticatedOrReadOnly]

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

IsAuthenticatedOrReadOnly — 記事の一覧 / 詳細は誰でも見られて、作成 / 修正 / 削除はログインユーザーのみ。

アクション別 permission — get_permissions #

同じ ViewSet の中で action ごとに別の権限が必要なら。

アクション別の権限
from rest_framework.permissions import IsAuthenticated, AllowAny


class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

    def get_permissions(self):
        if self.action in ["list", "retrieve"]:
            return [AllowAny()]
        return [IsAuthenticated()]

get_permissions()インスタンスのリスト を返します (クラスではなく)。() で呼び出す点に注意。

オブジェクト単位 permission — IsOwner を作る #

自分の記事だけを修正 / 削除できるべきです。オブジェクト単位の検査は has_object_permission メソッドで。

blog/permissions.py
from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):
    """オブジェクトの所有者だけが修正 / 削除。読み取りは誰でも。"""

    def has_permission(self, request, view):
        # コレクションレベル — 認証されたユーザーだけ書き込み
        if request.method in permissions.SAFE_METHODS:
            return True
        return request.user and request.user.is_authenticated

    def has_object_permission(self, request, view, obj):
        # オブジェクトレベル — 読み取りは誰でも
        if request.method in permissions.SAFE_METHODS:
            return True
        # 書き込みは所有者だけ
        return obj.author == request.user
blog/views.py
from .permissions import IsOwnerOrReadOnly


class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [IsOwnerOrReadOnly]
    ...

has_permission vs has_object_permission #

DRF は 2 段階で検査します。

  1. has_permission — view 進入時点。すべてのリクエストに対して。
  2. has_object_permissionget_object() 呼び出し後。retrieve/update/destroy のように単一オブジェクトを扱うとき。

list アクションはオブジェクトがないため has_object_permission が呼ばれません — list レスポンスで自分のものだけをフィルタリングするには get_queryset() で処理する必要があります。これは次回のテーマでもあります。

権限の組み合わせ — &|~ #

複数の permission を論理演算子で組み合わせられます。

組み合わせ
from rest_framework.permissions import IsAuthenticated, IsAdminUser


class PostViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticated & (IsOwnerOrReadOnly | IsAdminUser)]

「認証されたユーザー かつ (所有者または admin)」 — 自然な表現。

よく使うパターン — IsAdminOrReadOnly、role ベース #

blog/permissions.py を拡張
class IsAdminOrReadOnly(permissions.BasePermission):
    """admin だけ書き込み。誰でも読み取り。"""

    def has_permission(self, request, view):
        if request.method in permissions.SAFE_METHODS:
            return True
        return request.user and request.user.is_staff


class HasRole(permissions.BasePermission):
    """user.profile.role ベース。"""

    required_role: str = ""

    def has_permission(self, request, view):
        return (
            request.user
            and request.user.is_authenticated
            and getattr(request.user.profile, "role", None) == self.required_role
        )


def has_role(role: str):
    """ファクトリ — `HasRole` インスタンスを作るクラスを生成。"""
    return type(f"HasRole_{role}", (HasRole,), {"required_role": role})


class PostViewSet(viewsets.ModelViewSet):
    permission_classes = [has_role("editor")]

FastAPI #4 の require_role パターンと似た形 — 関数がクラスを返します。

Throttling — Bonus、認証と一緒に出てくるもの #

権限とは別に、リクエスト回数の制限 も同じところで一緒に押さえるのが一般的です。

settings.py
REST_FRAMEWORK = {
    ...,
    "DEFAULT_THROTTLE_CLASSES": [
        "rest_framework.throttling.AnonRateThrottle",
        "rest_framework.throttling.UserRateThrottle",
    ],
    "DEFAULT_THROTTLE_RATES": {
        "anon": "100/hour",
        "user": "1000/hour",
    },
}

匿名ユーザーは 1 時間あたり 100 回、認証されたユーザーは 1000 回。キャッシュバックエンドが必要です (CACHES 設定 — 上級 #4 キャッシング 参照)。

よく出会う落とし穴 #

1) 認証クラスの順序 #

🚫 Session が先だと CSRF が強制される
"DEFAULT_AUTHENTICATION_CLASSES": [
    "rest_framework.authentication.SessionAuthentication",   # 先
    "rest_framework_simplejwt.authentication.JWTAuthentication",
],

JWT だけを使うモバイル / SPA クライアントは普通 CSRF トークンを送りません。SessionAuth が先だと CSRF 検査に引っかかります。JWT を先 に置くか、SessionAuth を外してください。

2) DEFAULT_PERMISSION_CLASSES が空だと誰でも #

🚫 空だと AllowAny と同じ
"DEFAULT_PERMISSION_CLASSES": [],

明示的に IsAuthenticated をグローバルデフォルトに置き、公開エンドポイントは view ごとに [AllowAny] でオーバーライド — deny by default が安全です。

3) has_object_permission だけでは足りない #

🚫 has_permission の漏れ
class IsOwner(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.author == request.user

list アクションには呼ばれないので、グローバル権限検査を一緒に置かないと、未認証ユーザーが list を見られます。has_permission で認証の有無をまず検査してください。

4) JWT secret = Django SECRET_KEY のまま #

simplejwt はデフォルトで SECRET_KEY を使います。露出するとトークンの偽造が可能。SECRET_KEY 自体を環境変数で強く管理するか、SIGNING_KEY を別途設定してください。

settings.py
SIMPLE_JWT["SIGNING_KEY"] = os.environ["JWT_SIGNING_KEY"]

まとめ #

今回つかんだもの:

  • DRF 認証 vs Django 本体認証 — 同じ User を別のトランスポートで
  • 認証クラス — SessionAuthTokenAuthJWT (simplejwt)、OAuth2
  • DEFAULT_AUTHENTICATION_CLASSESDEFAULT_PERMISSION_CLASSES のグローバル設定
  • TokenAuthenticationauthtoken アプリ、signal で自動トークン生成、Token <key> ヘッダ
  • JWT (simplejwt)TokenObtainPairViewTokenRefreshView、refresh rotation、Bearer ヘッダ
  • 権限クラス — AllowAnyIsAuthenticatedIsAdminUserIsAuthenticatedOrReadOnly
  • permission_classes の view 別オーバーライド、get_permissions() のアクション別分岐
  • オブジェクト単位の権限has_permission + has_object_permission の 2 段階
  • IsOwnerOrReadOnly カスタムパターン
  • 権限の組み合わせ &|~
  • Throttling でリクエスト回数の制限
  • 落とし穴 — 認証順序、空の perm、list にオブジェクト権限が適用されない、JWT secret

FastAPI #4Depends(get_current_user) パターンが DRF では permission_classes + request.user で解かれます。依存性注入の豊かさは FastAPI が、ビルトイン権限システムの深さは Django/DRF が強いです。

次回 (#3 Filtering / Ordering / Pagination) では、大きなデータを扱う標準的な道具 — フィルタリング、ソート、ページネーション — を扱います。

X