Django + DRF로 JWT 인증 구현하기: 액세스/리프레시 토큰 제대로 다루기
요약: DRF에서 djangorestframework-simplejwt(이하 SimpleJWT)를 사용해 JWT 기반 인증을 구축하고, 리프레시 토큰 회전(rotation)·블랙리스트까지 포함한 실전 패턴을 정리합니다. 예제 코드와 함께 클라이언트 저장 전략, 로그아웃, 보안 체크리스트, 흔한 함정까지 한 번에 훑어봅니다.
왜 JWT인가?
- 장점: 서버 상태를 저장하지 않는(stateless) 확장성, 다양한 클라이언트(웹/모바일)에서 동일 방식 사용.
- 단점: 서버가 임의로 토큰 무효화하기 어렵다 → 리프레시 토큰 블랙리스트나 키 회전 같은 보완 장치가 필요.
선택: SimpleJWT
DRF와 잘 맞고, 토큰 쌍(Access/Refresh), 회전, 블랙리스트, 커스텀 클레임 등을 손쉽게 제공합니다.
bash
복사
pip install djangorestframework djangorestframework-simplejwt
python
복사
# settings.py INSTALLED_APPS = [ ..., 'rest_framework', 'rest_framework_simplejwt.token_blacklist', # 블랙리스트 사용 시 ] REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', ), } from datetime import timedelta SIMPLE_JWT = { "ACCESS_TOKEN_LIFETIME": timedelta(minutes=15), "REFRESH_TOKEN_LIFETIME": timedelta(days=14), "ROTATE_REFRESH_TOKENS": True, # 리프레시 회전 "BLACKLIST_AFTER_ROTATION": True, # 이전 리프레시 블랙리스트 "UPDATE_LAST_LOGIN": True, "ALGORITHM": "HS256", # "SIGNING_KEY": SECRET_KEY 기본 사용(별도 키 권장) "AUTH_HEADER_TYPES": ("Bearer",), }
URL과 기본 뷰 연결:
python
복사
# urls.py from django.urls import path from rest_framework_simplejwt.views import ( TokenObtainPairView, TokenRefreshView, TokenVerifyView, TokenBlacklistView ) urlpatterns = [ path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), path("api/token/verify/", TokenVerifyView.as_view(), name="token_verify"), path("api/token/blacklist/", TokenBlacklistView.as_view(), name="token_blacklist"), ]
마이그레이션:
bash
복사
python manage.py migrate
인증 흐름 한눈에
- 로그인: POST /api/token/ → access, refresh 발급
- API 호출: Authorization: Bearer <access>
- 만료 시 갱신: POST /api/token/refresh/ + <refresh>
- ROTATE_REFRESH_TOKENS=True이면 새 리프레시가 발급되고, 이전 것은 블랙리스트 처리
- 로그아웃: 서버에 리프레시 제출 → 블랙리스트에 등록
커스텀 클레임(예: username, role 추가)
python
복사
# auth/serializers.py from rest_framework_simplejwt.serializers import TokenObtainPairSerializer class MyTokenObtainPairSerializer(TokenObtainPairSerializer): @classmethod def get_token(cls, user): token = super().get_token(user) token["username"] = user.username token["role"] = getattr(user, "role", "user") return token
python
복사
# auth/views.py from rest_framework_simplejwt.views import TokenObtainPairView from .serializers import MyTokenObtainPairSerializer class MyTokenObtainPairView(TokenObtainPairView): serializer_class = MyTokenObtainPairSerializer
python
복사
# urls.py path("api/token/", MyTokenObtainPairView.as_view(), name="token_obtain_pair"),
주의: **민감정보(ID, 권한 외 개인정보)**는 클레임에 넣지 않습니다. 토큰은 클라이언트에 노출됩니다.
로그아웃(리프레시 무효화)
python
복사
# auth/views.py from rest_framework import status, permissions from rest_framework.response import Response from rest_framework.views import APIView from rest_framework_simplejwt.tokens import RefreshToken, TokenError class LogoutView(APIView): permission_classes = [permissions.IsAuthenticated] def post(self, request): refresh = request.data.get("refresh") if not refresh: return Response({"detail": "refresh required"}, status=400) try: token = RefreshToken(refresh) token.blacklist() return Response(status=status.HTTP_205_RESET_CONTENT) except TokenError: return Response({"detail": "invalid token"}, status=400)
python
복사
# urls.py path("api/logout/", LogoutView.as_view(), name="logout"),
클라이언트 저장 전략
- 권장: Access는 메모리(또는 JS 변수), Refresh는 HTTP-Only Secure 쿠키.
- XSS로부터 refresh 보호, Access 탈취 시에도 만료가 짧아 피해 제한.
- 쿠키 사용 시 CSRF 고려 필요:
- SameSite=strict/lax 설정
- DRF는 기본적으로 헤더 기반이므로, 쿠키에서 토큰을 꺼내 헤더로 붙이는 커스텀 미들웨어 or 엔드포인트 설계가 필요.
- 대안: 둘 다 메모리에 보관하고, 앱 재실행 시 재로그인. 보안은 강하지만 UX가 다소 불편.
프런트 인터셉터(의사 코드)
js
복사
// axios 예시(단순화) api.interceptors.response.use(undefined, async (error) => { if (error.response?.status === 401 && !error.config.__retry) { error.config.__retry = true; // refresh 쿠키 전송 또는 저장소에서 가져오기 const { data } = await api.post('/api/token/refresh/', { refresh: getRefresh() }); setAccess(data.access); // 갱신 error.config.headers.Authorization = `Bearer ${data.access}`; return api.request(error.config); } return Promise.reject(error); });
권한과 보호 라우트
python
복사
# views.py from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView from rest_framework.response import Response class MeView(APIView): permission_classes = [IsAuthenticated] def get(self, request): return Response({"id": request.user.id, "username": request.user.username})
python
복사
# urls.py path("api/me/", MeView.as_view()),
보안 체크리스트
- HTTPS 필수, 쿠키에는 Secure·HttpOnly·SameSite 적용.
- Access 만료는 짧게(10~30분), Refresh는 7~30일 선호.
- 회전+블랙리스트로 재사용 공격 방어.
- 비밀번호 변경/계정 비활성화 시: 해당 사용자의 모든 Outstanding Token을 블랙리스트 처리.
- 서명 키 관리: 운영/스테이징 분리, 필요 시 키 회전.
- 토큰에 민감정보 금지, 필요한 최소 클레임만.
- 브루트포스 방지: 로그인 엔드포인트에 throttle 적용.
python
복사
REST_FRAMEWORK["DEFAULT_THROTTLE_CLASSES"] = [ "rest_framework.throttling.UserRateThrottle", "rest_framework.throttling.AnonRateThrottle", ] REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = { "user": "1000/hour", "anon": "20/min", }
슬라이딩 토큰(대안)
SimpleJWT의 SlidingToken을 쓰면 리프레시 없이 하나의 토큰으로 연장 가능하지만, 미세 제어와 강제 무효화 측면에선 토큰 쌍 + 블랙리스트가 이해·운영이 더 쉽습니다.
흔한 함정
- AUTH_HEADER_TYPES와 실제 헤더 프리픽스(Bearer) 불일치.
- 서버/클라이언트 시간 불일치로 만료 오판 → 서버 기준 검사, 클럭 스큐(minute 단위) 고려.
- 회전 활성화했는데 블랙리스트 앱 미설치: 재사용 탐지가 안 됨.
- 로그아웃을 클라이언트 토큰 삭제만 처리: 서버 블랙리스트 누락.
간단 테스트
bash
복사
# 로그인 curl -X POST http://localhost:8000/api/token/ -d "username=alice&password=secret" # 보호 API 호출 curl -H "Authorization: Bearer <access>" http://localhost:8000/api/me/ # 리프레시 curl -X POST http://localhost:8000/api/token/refresh/ -H "Content-Type: application/json" -d '{"refresh":"<refresh>"}' # 로그아웃(블랙리스트) curl -X POST http://localhost:8000/api/logout/ -H "Authorization: Bearer <access>" -H "Content-Type: application/json" -d '{"refresh":"<refresh>"}'
마무리
- 기본: TokenObtainPair + TokenRefresh + Verify
- 실전: 회전 + 블랙리스트 + 로그아웃 엔드포인트
- 운영: HTTPS, 짧은 Access, 안전한 Refresh 저장, 토큰 무효화 전략
이 구성만 갖추면 DRF 기반 인증은 안정적으로 굴러갑니다. 이후에는 조직 요구에 맞춰 커스텀 클레임·권한, 이벤트 기반 무효화, 감사 로깅을 추가해 품질을 끌어올리면 됩니다.
'백엔드' 카테고리의 다른 글
Django ORM 기초: 개념과 예제 (2) | 2025.06.20 |
---|---|
Docker의 실무 활용 사례 및 사용법 (0) | 2025.06.09 |
Django 프로젝트의 settings.py 주요 설정과 활용법 (3) | 2025.06.04 |
Gunicorn을 사용하여 Django 배포하기 (0) | 2025.05.30 |
Django REST Framework: APIView, Generic View, ViewSet 차이점과 사용 예시 (0) | 2025.05.20 |