장고 실전 #2 인증/권한 — Token, JWT, custom permission
#1에서 만든 PostViewSet은 누구나 글을 쓰고 수정할 수 있는 상태입니다. 진짜 서비스가 되려면 두 질문에 답해야 합니다.
- 인증 (Authentication) — “당신이 누구인가?”
- 인가 (Authorization) — “그 사람이 이 일을 해도 되는가?”
DRF는 두 범주에 각각 **인증 클래스 (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는 한 요청에 여러 인증 클래스를 차례로 시도 합니다. 첫 번째로 성공한 인증을 채택합니다.
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는 두 단계로 검사합니다.
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",
},
}익명 사용자는 시간당 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_classesview 별 오버라이드,get_permissions()액션별 분기- 객체 단위 권한 —
has_permission+has_object_permission두 단계 IsOwnerOrReadOnly커스텀 패턴- 권한 조합
&,|,~ - Throttling으로 요청 횟수 제한
- 함정 — 인증 순서, 비어있는 perm, list에 객체 권한 적용 안 됨, JWT secret
FastAPI #4의 Depends(get_current_user) 패턴이 DRF에서는 permission_classes + request.user로 풀립니다. 의존성 주입의 풍부함은 FastAPI가, 빌트인 권한 시스템의 깊이는 Django/DRF가 더 강합니다.
다음 글(#3 Filtering / Ordering / Pagination)에서는 큰 데이터를 다루는 표준 도구들 — 필터링, 정렬, 페이지네이션 — 을 다룹니다.