Django DRF #2 認証 / 権限 — Token、JWT、custom permission
#1 で作った PostViewSet は誰でも記事を書いて修正できる状態です。本物のサービスにするには 2 つの問いに答える必要があります。
- 認証 (Authentication) — 「あなたは誰か?」
- 認可 (Authorization) — 「その人がこの仕事をしてよいか?」
DRF は 2 か所にそれぞれ 認証クラス (authentication classes) と 権限クラス (permission classes) を持ちます。両者は完全に分離した概念ですが、一緒に動作します。
DRF 認証 vs Django 本体の認証 #
中級 #4 ユーザー / 権限 の Django 本体認証と DRF 認証は位置づけが違います。
| Django 本体認証 | DRF 認証 | |
|---|---|---|
| 主な用途 | サーバーレンダリングのページ | JSON API |
| 保存場所 | session + cookie | header (Token / JWT) |
| ミドルウェア | AuthenticationMiddleware | DEFAULT_AUTHENTICATION_CLASSES |
| ログイン form | LoginView (template) | obtain_auth_token / JWT view |
request.user | ミドルウェアが埋める | 認証クラスが埋める |
| 一緒に使えるか | — | はい (SessionAuth も DRF がサポート) |
同じ User モデルを共有し、Django の is_authenticated、permissions のようなものもそのまま使います。認証のトランスポート (どこから資格を取ってくるか) だけが違います。
DRF の認証クラス #
DRF は 1 つのリクエストに 複数の認証クラスを順に試行 します。最初に成功した認証を採択します。
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 がメール / パスワードでトークンを発行してくれます。
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 migratefrom 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 #
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-simplejwtREST_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_APPS に rest_framework_simplejwt.token_blacklist を追加)。
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()),
]発行の流れ #
curl -X POST http://localhost:8000/api/auth/token/ \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"secret"}'{
"access": "eyJhbGciOi...",
"refresh": "eyJhbGciOi..."
}curl http://localhost:8000/api/posts/ \
-H "Authorization: Bearer eyJhbGciOi..."Refresh #
Access が失効したら (30 分後)、refresh トークンで新しい access を受け取ります。
curl -X POST http://localhost:8000/api/auth/token/refresh/ \
-H "Content-Type: application/json" \
-d '{"refresh":"eyJhbGciOi..."}'{"access": "eyJhbGciOi... 新しいトークン..."}TokenAuthentication vs JWT #
| TokenAuth | JWT (simplejwt) | |
|---|---|---|
| 保存場所 | DB | クライアントのみ |
| 検証コスト | DB クエリ | 署名検証 (メモリ) |
| 有効期限 | なし (手動削除) | 自動 (exp) |
| Refresh | なし | あり |
| 失効 | トークン削除 → 即時 | 期限まで、またはブラックリスト |
| ヘッダ prefix | Token | Bearer |
| 向く場面 | 内部ツール、シンプル | 一般ユーザー向けサービス |
Permission クラス #
認証が終わったら (request.user が埋まった後)、その人がこの動作をしてよいか? を検査するのが permission。
ビルトインの permission クラス #
| クラス | 意味 |
|---|---|
AllowAny | 誰でも (認証しなくてよい) |
IsAuthenticated | ログインしたユーザーのみ |
IsAdminUser | is_staff=True のみ |
IsAuthenticatedOrReadOnly | 読み取りは誰でも、書き込みはログインユーザーのみ |
DjangoModelPermissions | Django の add_、change_、delete_ 権限 (中級 #4) |
DjangoObjectPermissions | オブジェクト単位の権限 (django-guardian と一緒に) |
View 別オーバーライド #
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 メソッドで。
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.userfrom .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 段階で検査します。
has_permission— view 進入時点。すべてのリクエストに対して。has_object_permission—get_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 ベース
#
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、認証と一緒に出てくるもの #
権限とは別に、リクエスト回数の制限 も同じところで一緒に押さえるのが一般的です。
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) 認証クラスの順序 #
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication", # 先
"rest_framework_simplejwt.authentication.JWTAuthentication",
],JWT だけを使うモバイル / SPA クライアントは普通 CSRF トークンを送りません。SessionAuth が先だと CSRF 検査に引っかかります。JWT を先 に置くか、SessionAuth を外してください。
2) DEFAULT_PERMISSION_CLASSES が空だと誰でも
#
"DEFAULT_PERMISSION_CLASSES": [],明示的に IsAuthenticated をグローバルデフォルトに置き、公開エンドポイントは view ごとに [AllowAny] でオーバーライド — deny by default が安全です。
3) has_object_permission だけでは足りない
#
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj.author == request.userlist アクションには呼ばれないので、グローバル権限検査を一緒に置かないと、未認証ユーザーが list を見られます。has_permission で認証の有無をまず検査してください。
4) JWT secret = Django SECRET_KEY のまま #
simplejwt はデフォルトで SECRET_KEY を使います。露出するとトークンの偽造が可能。SECRET_KEY 自体を環境変数で強く管理するか、SIGNING_KEY を別途設定してください。
SIMPLE_JWT["SIGNING_KEY"] = os.environ["JWT_SIGNING_KEY"]まとめ #
今回つかんだもの:
- DRF 認証 vs Django 本体認証 — 同じ User を別のトランスポートで
- 認証クラス —
SessionAuth、TokenAuth、JWT (simplejwt)、OAuth2 DEFAULT_AUTHENTICATION_CLASSES、DEFAULT_PERMISSION_CLASSESのグローバル設定- TokenAuthentication —
authtokenアプリ、signal で自動トークン生成、Token <key>ヘッダ - JWT (simplejwt) —
TokenObtainPairView、TokenRefreshView、refresh rotation、Bearerヘッダ - 権限クラス —
AllowAny、IsAuthenticated、IsAdminUser、IsAuthenticatedOrReadOnly permission_classesの view 別オーバーライド、get_permissions()のアクション別分岐- オブジェクト単位の権限 —
has_permission+has_object_permissionの 2 段階 IsOwnerOrReadOnlyカスタムパターン- 権限の組み合わせ
&、|、~ - Throttling でリクエスト回数の制限
- 落とし穴 — 認証順序、空の perm、list にオブジェクト権限が適用されない、JWT secret
FastAPI #4 の Depends(get_current_user) パターンが DRF では permission_classes + request.user で解かれます。依存性注入の豊かさは FastAPI が、ビルトイン権限システムの深さは Django/DRF が強いです。
次回 (#3 Filtering / Ordering / Pagination) では、大きなデータを扱う標準的な道具 — フィルタリング、ソート、ページネーション — を扱います。