DRF 소스 코드로 여행하기: 내가 커스텀 인증기만 만들던 이유와 '내장 인증기'의 재발견
1. 저는 DRF를 사랑하지만, 내장 인증기는 믿지 않았습니다.
Django와 DRF(Django Rest Framework)는 제 개발 인생의 동반자입니다. 하지만 인증(Authentication)에 있어서만큼은 늘 고집스러운 면이 있었습니다. 저는 주로 JWT, API Key, 혹은 OAuth2 방식을 선호합니다. 최신 앱 환경에서는 이것이 표준이니까요.
그런데 이상하게도 DRF 공식 문서를 펴보면, 정작 제가 쓰는 현대적인 방식들보다는 Basic, Session, Token, Remote 같은 조금은 '올드'해 보이는 인증 클래스들이 메인을 차지하고 있습니다. "유능한 DRF가 왜 굳이 이런 걸 내장하고 있을까?"라는 의구심이 들 때가 많았죠. 그래서 저는 항상 BaseAuthentication을 상속받아 저만의 커스텀 인증기를 만들어 쓰곤 했습니다.
2. 왜 다시 '내장 인증기'의 소스를 열어보는가?
커스텀 인증기를 만들다 보니 문득 이런 생각이 들었습니다.
"내가 만든 인증기가 과연 Django와 DRF의 철학에 완벽하게 녹아들어 있는가?"
단순히 request.user에 유저를 담아주는 기능을 넘어, DRF가 설계한 인증 스키마의 '자연스러움'을 놓치고 있는 건 아닐까 하는 고민이 생겼죠. 그래서 오늘부터 수회에 걸쳐 DRF의 내장 인증기 4가지를 낱낱이 파헤쳐 보려 합니다.
- BasicAuthentication (HTTP 기본 인증)
- SessionAuthentication (Django 세션 활용)
- TokenAuthentication (단순 토큰 인증)
- RemoteUserAuthentication (외부 인증 연동)
3. '껄끄러움' 뒤에 숨겨진 '철학' 찾기
솔직히 말해, 최신 서비스에서 BasicAuthentication을 그대로 쓰기는 껄끄럽습니다. 보안에 취약해 보이고, 매번 아이디와 비번을 실어 보내는 구조가 불안하죠. 하지만 그 소스 코드를 뜯어보면 '인증 실패 시 클라이언트와 어떻게 소통할 것인가'에 대한 아주 정교한 설계가 담겨 있습니다.
예를 들어, 단순히 403 Forbidden을 던지는 것과 authenticate_header를 통해 표준 규격인 401 Unauthorized를 유도하는 차이 같은 것 말이죠. 이런 디테일이 바로 "Django다운" 클래스를 만드는 핵심이었습니다.
4. 이 시리즈의 목적: 커스텀을 '자연스럽게' 만들기
이 시리즈의 끝에서 제가 얻고자 하는 것은 단순한 지식이 아닙니다.
- 영감(Inspiration): 내장 인증기들이 왜 그런 구조로 짜여 있는지, 그 설계 철학을 배웁니다.
- 이식(Porting): 그 철학을 제가 즐겨 쓰는 JWT나 OAuth2 커스텀 인증기에 이식합니다.
- 조화(Harmony): 그리하여 제가 만든 커스텀 인증기가 DRF의 인증 시스템 안에서 마치 처음부터 거기 있었던 것처럼 자연스럽게 작동하게 만드는 것입니다.
단순히 기능을 구현하는 개발자를 넘어, 프레임워크의 철학을 코드에 녹여낼 수 있는 개발자가 되기 위한 첫걸음. DRF 인증기 소스 코드 분석 시리즈, 지금 시작합니다.

5. 소스 코드 들여다보기: BasicAuthentication
서두가 길었습니다. 본격적으로 BasicAuthentication의 심장부를 열어보겠습니다.
DRF의 rest_framework/authentication.py를 열어보면 이 클래스의 실체를 마주할 수 있습니다. 생각보다 간결하지만, 그 안에는 탄탄한 규칙이 담겨 있죠.
class BasicAuthentication(BaseAuthentication):
"""
HTTP Basic authentication against username/password.
"""
www_authenticate_realm = 'api'
def authenticate(self, request):
auth = get_authorization_header(request).split()
if not auth or auth[0].lower() != b'basic':
return None
if len(auth) == 1:
msg = _('Invalid basic header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = _('Invalid basic header. Credentials string should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)
try:
try:
auth_decoded = base64.b64decode(auth[1]).decode('utf-8')
except UnicodeDecodeError:
auth_decoded = base64.b64decode(auth[1]).decode('latin-1')
userid, password = auth_decoded.split(':', 1)
except (TypeError, ValueError, UnicodeDecodeError, binascii.Error):
msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
raise exceptions.AuthenticationFailed(msg)
return self.authenticate_credentials(userid, password, request)
def authenticate_credentials(self, userid, password, request=None):
credentials = {
get_user_model().USERNAME_FIELD: userid,
'password': password
}
user = authenticate(request=request, **credentials)
if user is None:
raise exceptions.AuthenticationFailed(_('Invalid username/password.'))
if not user.is_active:
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
return (user, None)
def authenticate_header(self, request):
return 'Basic realm="%s"' % self.www_authenticate_realm
왜 하필 헤더(Header)인가?
우리는 보통 데이터를 보낼 때 request.POST나 request.data를 먼저 떠올립니다. 하지만 인증 정보만큼은 헤더에 담는 것이 국룰입니다. 왜일까요?
인증은 모든 HTTP 메서드(GET, POST, PUT, DELETE 등)에 유연하게 대응해야 하기 때문입니다. GET 요청은 Body가 아예 없고, DELETE 역시 본문이 비어있는 경우가 많죠. 인증 정보를 Body에 넣어야 한다면 우리는 조회를 할 때도 억지로 POST를 써야 하는 기괴한 설계를 해야 했을 겁니다. 헤더에 인증을 담는 것은 "어떤 문을 열든 신분증은 손에 들고 있어라"는 약속인 셈입니다.
냉정한 현실: 보안의 취약점
이 방식의 최대 단점은 Base64입니다. 암호화가 아닌 단순 '인코딩'이죠. 누구나 디코딩 사이트에서 1초 만에 당신의 아이디와 비번을 알아낼 수 있습니다.
결론: HTTP 환경에서는 절대 사용 금지입니다. 오직 HTTPS라는 안전한 터널 안에서만, 그것도 매우 제한적인 용도로만 사용해야 합니다.
첫 번째 영감: 머신 간 통신(M2M)의 가성비 보안
그럼에도 이 낡은 인증기가 매력적인 순간이 있습니다. 바로 서버와 서버가 통신할 때입니다. HMAC이나 복잡한 API Key 시스템을 구축하기엔 리소스가 아깝고, 그렇다고 생으로 데이터를 보내긴 불안할 때 우리는 이 구조를 '살짝' 비틀어볼 수 있습니다.
- 커스텀 암호화: Base64 대신, 양측 서버만 알고 있는 대칭키로 암호화된 바이너리 컨텐츠를 전송합니다.
- 간편한 구현: 복잡한 핸드쉐이크 없이도
Basic <Encrypted_Data>형태만으로 빠르고 안전한 통신이 가능해집니다. "우리끼리만 아는 암호"를 주고받는 효율적인 통로가 되는 것이죠.
두 번째 영감: 잃어버린 톱니바퀴 authenticate_header()
가장 눈여겨볼 메서드는 바로 authenticate_header()입니다. 소스 코드 어디를 봐도 이 녀석을 직접 부르는 곳은 없습니다. 하지만 이 녀석은 DRF 예외 처리기 속에서 '401 Unauthorized'를 만들어내는 핵심 열쇠입니다.
커스텀 인증기를 만들 때 우리가 흔히 간과하는 것이 이 메서드를 빼먹는 것입니다. 사실 이 메서드를 빼먹어도 인증(Authentication)이라는 행위 자체에는 아무런 영향이 없기 때문입니다. 하지만 이 사소한 차이가 뷰(View) 단의 코드를 얼마나 지저분하게 만드는지 비교해 볼 필요가 있습니다.
Case 1. authenticate_header를 생략했을 때 (수동 방식)
이 메서드가 없으면 DRF는 "어떤 방식으로 인증해야 할지 가이드를 줄 수 없다"고 판단하여, 인증 실패 시 401이 아닌 403 Forbidden을 던집니다. 이를 피하려면 뷰에서 아래와 같은 '노가다'가 필요합니다.
# ❌ authenticate_header가 없는 경우
class MyPostAPIView(APIView):
# IsAuthenticated를 쓰면 무조건 403이 나가므로, 401을 위해 일단 열어줍니다.
permission_classes = [AllowAny]
def post(self, request):
# 뷰 내부에서 일일이 인증 여부를 확인하고 401을 수동으로 반환해야 합니다.
if not request.user or not request.user.is_authenticated:
return Response(
{"detail": "로그인이 필요합니다."},
status=status.HTTP_401_UNAUTHORIZED
)
# ... 실제 로직 시작 ...
Case 2. authenticate_header를 구현했을 때 (DRF 철학의 구현)
반면, 커스텀 인증기에 단 한 줄의 안내문(authenticate_header)을 적어두면, DRF의 거대한 인증 스키마가 이를 인지하고 알아서 401 응답을 조립합니다.
# ✅ authenticate_header가 있는 경우
class MyPostAPIView(APIView):
# 이제 DRF가 알아서 401을 던져줄 것이므로, 선언적으로 권한을 제어할 수 있습니다.
permission_classes = [IsAuthenticated]
def post(self, request):
# 뷰는 오직 '비즈니스 로직'에만 집중합니다.
# 인증 실패 처리는 이미 윗단(인증기의 authenticate_header메서드)에서 끝났기 때문입니다.
...
이것이야말로 "Django스러운" 미학입니다. 파편화된 코드를 뷰에 흩뿌리는 대신, 프레임워크가 설계한 톱니바퀴 사이에 내 코드를 완벽하게 끼워 넣는 느낌이죠. 이 메서드 하나로 우리는 '인증'과 '인가'의 역할을 명확히 분리하고, 뷰를 한결 가볍게 유지할 수 있습니다.
DRF의 진짜 매력: 조용한 양보 (None의 미학)
마지막으로 소스 코드의 이 부분을 다시 보시죠.
if not auth or auth[0].lower() != b'basic':
return None
형식이 맞지 않을 때 에러를 내뿜는 대신 None을 반환하며 조용히 뒤로 빠집니다. 이 사소한 처리가 DRF의 유연성을 만듭니다. 덕분에 우리는 AUTHENTICATION_CLASSES 리스트에 여러 인증기를 순서대로 담을 수 있습니다. "아, 내 방식이 아니네? 그럼 다음 인증기 확인해봐!"라고 배려하는 개발자들의 흔적이 느껴지지 않나요?
마무리
BasicAuthentication은 구식처럼 보이지만, 그 구조만큼은 현대적인 인증 시스템의 근간을 이루고 있습니다. 특히 예외 처리와 다중 인증 지원 방식은 우리가 커스텀 인증기를 만들 때 반드시 흡수해야 할 자산입니다.
자, 이제 첫 번째 산을 넘었습니다. 다음 포스트에서는 우리에게 익숙하면서도 여전히 아리송한 SessionAuthentication을 파헤쳐 보겠습니다.
"당신이 세션 인증을 쓰면서도 몰랐던 사실들", 기대해 주세요!
댓글이 없습니다.