drf ·

DRF JWT 인증 커스터마이징 - SimpleJWT 실전 가이드

DRF의 기본 인증은 Session과 Token 방식이다. 하지만 SPA나 모바일 앱과 통신하는 API 서버라면 JWT(JSON Web Token)가 표준이 되었다. djangorestframework-simplejwt를 기본 설정 그대로 쓰지 말고, 실무에 맞게 커스터마이징하는 방법을 정리한다.

SimpleJWT 기본 설정

pip install djangorestframework-simplejwt
# settings.py
INSTALLED_APPS = [
    ...
    'rest_framework_simplejwt',
    'rest_framework_simplejwt.token_blacklist',  # 토큰 무효화 지원
]

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ),
}

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

핵심 설정을 하나씩 보자.

Access Token과 Refresh Token

클라이언트                    서버
   │                          │
   ├── POST /auth/login ─────►│ (아이디/비밀번호)
   │◄── access + refresh ─────│
   │                          │
   ├── GET /api/users ────────►│ (Authorization: Bearer <access>)
   │◄── 200 OK ───────────────│
   │                          │
   │  ... 30분 후 access 만료 ...
   │                          │
   ├── GET /api/users ────────►│ (만료된 access)
   │◄── 401 Unauthorized ─────│
   │                          │
   ├── POST /auth/refresh ────►│ (refresh token)
   │◄── 새 access + refresh ──│
   │                          │
  • Access Token: 짧은 수명 (30분). 매 요청에 사용
  • Refresh Token: 긴 수명 (30일). Access Token 갱신에만 사용

ROTATE_REFRESH_TOKENS

True로 설정하면 refresh 요청 시 새 refresh token도 함께 발급한다. 클라이언트는 항상 최신 refresh token을 저장해야 한다.

BLACKLIST_AFTER_ROTATION

True로 설정하면 이전 refresh token을 블랙리스트에 등록한다. 탈취된 refresh token으로 재발급이 불가능해진다. 이를 위해 rest_framework_simplejwt.token_blacklist 앱이 필요하다.

python manage.py migrate  # 블랙리스트 테이블 생성

Custom Authentication 클래스

SimpleJWT의 기본 JWTAuthentication은 Django의 User 모델 하나만 지원한다. 실무에서는 내부 사용자와 외부 클라이언트 등 여러 유저 타입을 처리해야 할 수 있다.

from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework_simplejwt.exceptions import InvalidToken, AuthenticationFailed


class CustomJWTAuthentication(JWTAuthentication):
    def authenticate(self, request):
        try:
            return super().authenticate(request)
        except (InvalidToken, AuthenticationFailed):
            raise
        except Exception as e:
            logger.warning(f"JWT authentication error: {e}")
            raise AuthenticationFailed("인증 처리 중 오류가 발생했습니다")

    def get_user(self, validated_token):
        user_type = validated_token.get("user_type", "user")
        user_id = validated_token.get("user_seq")

        if not user_id:
            raise InvalidToken("토큰에 사용자 정보가 없습니다")

        if user_type == "user":
            user = InternalUser.objects.filter(pk=user_id).first()
        elif user_type == "client":
            user = ClientUser.objects.filter(pk=user_id).first()
        else:
            raise InvalidToken(f"알 수 없는 사용자 유형: {user_type}")

        if user is None:
            raise AuthenticationFailed("사용자를 찾을 수 없습니다")

        return user

get_user()를 오버라이드해서 토큰의 user_type 클레임에 따라 다른 모델에서 사용자를 조회한다.

커스텀 토큰 클레임

기본 JWT payload에 추가 정보를 넣으려면:

from rest_framework_simplejwt.serializers import TokenObtainPairSerializer


class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
    @classmethod
    def get_token(cls, user):
        token = super().get_token(user)

        # 커스텀 클레임 추가
        token["user_type"] = "user"
        token["user_seq"] = user.pk
        token["email"] = user.email
        token["role"] = user.role

        return token

View에서 사용:

from rest_framework_simplejwt.views import TokenObtainPairView


class CustomTokenObtainPairView(TokenObtainPairView):
    serializer_class = CustomTokenObtainPairSerializer

Multi-Backend 인증

Django의 AUTHENTICATION_BACKENDS를 활용해 여러 인증 방식을 지원할 수 있다:

# settings.py
AUTHENTICATION_BACKENDS = [
    "apps.auth.backends.MultiUserBackend",
]
from django.contrib.auth.backends import BaseBackend


class MultiUserBackend(BaseBackend):
    def authenticate(self, request, username=None, password=None, user_type=None, **kwargs):
        if user_type == "user":
            # 내부 사용자: 이메일로 조회
            try:
                user = InternalUser.objects.get(email=username)
                if user.check_password(password):
                    return user
            except InternalUser.DoesNotExist:
                return None

        elif user_type == "client":
            # 외부 클라이언트: 아이디로 조회
            try:
                user = ClientUser.objects.get(username=username)
                if user.check_password(password):
                    return user
            except ClientUser.DoesNotExist:
                return None

        return None

인증 엔드포인트 구성

# urls.py
urlpatterns = [
    # 내부 사용자
    path("auth/login", LoginView.as_view()),
    path("auth/refresh", TokenRefreshView.as_view()),
    path("auth/logout", LogoutView.as_view()),

    # 외부 클라이언트 (OTP 기반)
    path("clients/otp/request", OtpRequestView.as_view()),
    path("clients/otp/verify", OtpVerifyView.as_view()),
]

로그아웃은 refresh token을 블랙리스트에 추가하는 방식으로 구현한다:

from rest_framework_simplejwt.tokens import RefreshToken


class LogoutView(APIView):
    permission_classes = [IsAuthenticated]

    def post(self, request):
        refresh = request.data.get("refresh")
        if not refresh:
            return fail(code="INVALID_REQUEST", message="refresh 토큰이 필요합니다")

        try:
            token = RefreshToken(refresh)
            token.blacklist()
            return ok(code="LOGGED_OUT", message="로그아웃 완료", request=request)
        except Exception:
            return fail(code="INVALID_TOKEN", message="유효하지 않은 토큰입니다")

USER_ID_FIELD 설정

Django 기본 User 모델의 PK가 아닌 다른 필드를 사용한다면:

SIMPLE_JWT = {
    "USER_ID_FIELD": "user_seq",      # 모델의 PK 필드명
    "USER_ID_CLAIM": "user_seq",      # JWT payload의 클레임 키
}

커스텀 User 모델에서 PK 필드명이 id가 아닌 user_seq일 때 이 설정이 필요하다.

보안 체크리스트

  • Access Token 수명을 15~30분으로 제한
  • Refresh Token rotation 활성화 (ROTATE_REFRESH_TOKENS = True)
  • 토큰 블랙리스트 활성화 (BLACKLIST_AFTER_ROTATION = True)
  • HTTPS 전용 환경에서 운영
  • Refresh Token은 httpOnly cookie 또는 안전한 저장소에 보관
  • 토큰에 민감 정보 (비밀번호, 개인정보) 포함하지 않기
  • 만료된 블랙리스트 토큰 주기적 정리 (python manage.py flushexpiredtokens)

정리

설정기본값권장값이유
ACCESS_TOKEN_LIFETIME5분15~30분UX와 보안의 균형
REFRESH_TOKEN_LIFETIME1일7~30일사용 패턴에 따라
ROTATE_REFRESH_TOKENSFalseTrue토큰 탈취 방지
BLACKLIST_AFTER_ROTATIONFalseTrue이전 토큰 무효화
ALGORITHMHS256HS256단일 서버면 충분

SimpleJWT는 기본 설정이 보수적이어서 실무 환경에 맞게 튜닝이 필요하다. 특히 다중 유저 타입, 커스텀 클레임, 토큰 블랙리스트는 거의 모든 프로젝트에서 필요한 커스터마이징이다.