백엔드

Django + DRF로 JWT 인증 구현 액세스, 리프레시 토큰 제대로 다루기

지식소 채움이 2025. 8. 8. 09:45

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

인증 흐름 한눈에

  1. 로그인: POST /api/token/ → access, refresh 발급
  2. API 호출: Authorization: Bearer <access>
  3. 만료 시 갱신: POST /api/token/refresh/ + <refresh>
    • ROTATE_REFRESH_TOKENS=True이면 새 리프레시가 발급되고, 이전 것은 블랙리스트 처리
  4. 로그아웃: 서버에 리프레시 제출 → 블랙리스트에 등록

커스텀 클레임(예: 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 기반 인증은 안정적으로 굴러갑니다. 이후에는 조직 요구에 맞춰 커스텀 클레임·권한, 이벤트 기반 무효화, 감사 로깅을 추가해 품질을 끌어올리면 됩니다.