# DRF 소스 코드로 여행하기: 내가 커스텀 인증기만 만들던 이유와 '내장 인증기'의 재발견 ## 1. 저는 DRF를 사랑하지만, 내장 인증기는 믿지 않았습니다. {#sec-c2385e3e4312} Django와 DRF(Django Rest Framework)는 제 개발 인생의 동반자입니다. 하지만 인증(Authentication)에 있어서만큼은 늘 고집스러운 면이 있었습니다. 저는 주로 **JWT**, **API Key**, 혹은 **OAuth2** 방식을 선호합니다. 최신 앱 환경에서는 이것이 표준이니까요. 그런데 이상하게도 DRF 공식 문서를 펴보면, 정작 제가 쓰는 현대적인 방식들보다는 **Basic, Session, Token, Remote** 같은 조금은 '올드'해 보이는 인증 클래스들이 메인을 차지하고 있습니다. "유능한 DRF가 왜 굳이 이런 걸 내장하고 있을까?"라는 의구심이 들 때가 많았죠. 그래서 저는 항상 `BaseAuthentication`을 상속받아 저만의 커스텀 인증기를 만들어 쓰곤 했습니다. ## 2. 왜 다시 '내장 인증기'의 소스를 열어보는가? {#sec-331b1362b7e2} 커스텀 인증기를 만들다 보니 문득 이런 생각이 들었습니다. > "내가 만든 인증기가 과연 Django와 DRF의 철학에 완벽하게 녹아들어 있는가?" 단순히 `request.user`에 유저를 담아주는 기능을 넘어, DRF가 설계한 인증 스키마의 '자연스러움'을 놓치고 있는 건 아닐까 하는 고민이 생겼죠. 그래서 오늘부터 수회에 걸쳐 DRF의 내장 인증기 4가지를 낱낱이 파헤쳐 보려 합니다. * **BasicAuthentication** (HTTP 기본 인증) * **SessionAuthentication** (Django 세션 활용) * **TokenAuthentication** (단순 토큰 인증) * **RemoteUserAuthentication** (외부 인증 연동) ## 3. '껄끄러움' 뒤에 숨겨진 '철학' 찾기 {#sec-e9f76a8db02e} 솔직히 말해, 최신 서비스에서 `BasicAuthentication`을 그대로 쓰기는 껄끄럽습니다. 보안에 취약해 보이고, 매번 아이디와 비번을 실어 보내는 구조가 불안하죠. 하지만 그 소스 코드를 뜯어보면 **'인증 실패 시 클라이언트와 어떻게 소통할 것인가'**에 대한 아주 정교한 설계가 담겨 있습니다. 예를 들어, 단순히 `403 Forbidden`을 던지는 것과 `authenticate_header`를 통해 표준 규격인 `401 Unauthorized`를 유도하는 차이 같은 것 말이죠. 이런 디테일이 바로 "Django다운" 클래스를 만드는 핵심이었습니다. ## 4. 이 시리즈의 목적: 커스텀을 '자연스럽게' 만들기 {#sec-00b6ee52d1d1} 이 시리즈의 끝에서 제가 얻고자 하는 것은 단순한 지식이 아닙니다. 1. **영감(Inspiration):** 내장 인증기들이 왜 그런 구조로 짜여 있는지, 그 설계 철학을 배웁니다. 2. **이식(Porting):** 그 철학을 제가 즐겨 쓰는 JWT나 OAuth2 커스텀 인증기에 이식합니다. 3. **조화(Harmony):** 그리하여 제가 만든 커스텀 인증기가 DRF의 인증 시스템 안에서 마치 처음부터 거기 있었던 것처럼 자연스럽게 작동하게 만드는 것입니다. 단순히 기능을 구현하는 개발자를 넘어, 프레임워크의 철학을 코드에 녹여낼 수 있는 개발자가 되기 위한 첫걸음. **DRF 인증기 소스 코드 분석 시리즈**, 지금 시작합니다. ![개발자가 오래된 자물쇠를 해부하고 있는 이미지](/media/whitedec/blog_img/db4a2af117c3414395e86a0210d28f4a.webp) ## 5. 소스 코드 들여다보기: `BasicAuthentication` {#sec-d3e96fecb1fa} 서두가 길었습니다. 본격적으로 **BasicAuthentication**의 심장부를 열어보겠습니다. --- DRF의 `rest_framework/authentication.py`를 열어보면 이 클래스의 실체를 마주할 수 있습니다. 생각보다 간결하지만, 그 안에는 탄탄한 규칙이 담겨 있죠. ```python 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)인가?** {#sec-d4f681d28784} 우리는 보통 데이터를 보낼 때 `request.POST`나 `request.data`를 먼저 떠올립니다. 하지만 인증 정보만큼은 **헤더**에 담는 것이 국룰입니다. 왜일까요? 인증은 **모든 HTTP 메서드(GET, POST, PUT, DELETE 등)에 유연하게 대응**해야 하기 때문입니다. `GET` 요청은 Body가 아예 없고, `DELETE` 역시 본문이 비어있는 경우가 많죠. 인증 정보를 Body에 넣어야 한다면 우리는 조회를 할 때도 억지로 POST를 써야 하는 기괴한 설계를 해야 했을 겁니다. 헤더에 인증을 담는 것은 **"어떤 문을 열든 신분증은 손에 들고 있어라"**는 약속인 셈입니다. --- ### 냉정한 현실: 보안의 취약점 {#sec-28526421f804} 이 방식의 최대 단점은 **Base64**입니다. 암호화가 아닌 단순 '인코딩'이죠. 누구나 디코딩 사이트에서 1초 만에 당신의 아이디와 비번을 알아낼 수 있습니다. > **결론:** HTTP 환경에서는 절대 사용 금지입니다. 오직 HTTPS라는 안전한 터널 안에서만, 그것도 매우 제한적인 용도로만 사용해야 합니다. --- ### 첫 번째 영감: 머신 간 통신(M2M)의 가성비 보안 {#sec-999544506ed4} 그럼에도 이 낡은 인증기가 매력적인 순간이 있습니다. 바로 **서버와 서버가 통신할 때**입니다. HMAC이나 복잡한 API Key 시스템을 구축하기엔 리소스가 아깝고, 그렇다고 생으로 데이터를 보내긴 불안할 때 우리는 이 구조를 '살짝' 비틀어볼 수 있습니다. * **커스텀 암호화:** Base64 대신, 양측 서버만 알고 있는 대칭키로 암호화된 바이너리 컨텐츠를 전송합니다. * **간편한 구현:** 복잡한 핸드쉐이크 없이도 `Basic ` 형태만으로 빠르고 안전한 통신이 가능해집니다. "우리끼리만 아는 암호"를 주고받는 효율적인 통로가 되는 것이죠. --- ### **두 번째 영감: 잃어버린 톱니바퀴 `authenticate_header()`** {#sec-8f845e80ab36} 가장 눈여겨볼 메서드는 바로 `authenticate_header()`입니다. 소스 코드 어디를 봐도 이 녀석을 직접 부르는 곳은 없습니다. 하지만 이 녀석은 DRF 예외 처리기 속에서 **'401 Unauthorized'를 만들어내는 핵심 열쇠**입니다. 커스텀 인증기를 만들 때 우리가 흔히 간과하는 것이 이 메서드를 빼먹는 것입니다. 사실 이 메서드를 빼먹어도 **인증(Authentication)**이라는 행위 자체에는 아무런 영향이 없기 때문입니다. 하지만 이 사소한 차이가 뷰(View) 단의 코드를 얼마나 지저분하게 만드는지 비교해 볼 필요가 있습니다. #### **Case 1. `authenticate_header`를 생략했을 때 (수동 방식)** 이 메서드가 없으면 DRF는 "어떤 방식으로 인증해야 할지 가이드를 줄 수 없다"고 판단하여, 인증 실패 시 `401`이 아닌 `403 Forbidden`을 던집니다. 이를 피하려면 뷰에서 아래와 같은 '노가다'가 필요합니다. ```Python # ❌ 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` 응답을 조립합니다. ```Python # ✅ authenticate_header가 있는 경우 class MyPostAPIView(APIView): # 이제 DRF가 알아서 401을 던져줄 것이므로, 선언적으로 권한을 제어할 수 있습니다. permission_classes = [IsAuthenticated] def post(self, request): # 뷰는 오직 '비즈니스 로직'에만 집중합니다. # 인증 실패 처리는 이미 윗단(인증기의 authenticate_header메서드)에서 끝났기 때문입니다. ... ``` 이것이야말로 **"Django스러운"** 미학입니다. 파편화된 코드를 뷰에 흩뿌리는 대신, 프레임워크가 설계한 톱니바퀴 사이에 내 코드를 완벽하게 끼워 넣는 느낌이죠. 이 메서드 하나로 우리는 '인증'과 '인가'의 역할을 명확히 분리하고, 뷰를 한결 가볍게 유지할 수 있습니다. --- ### DRF의 진짜 매력: 조용한 양보 (`None`의 미학) {#sec-38c90c2a7f2a} 마지막으로 소스 코드의 이 부분을 다시 보시죠. ```python if not auth or auth[0].lower() != b'basic': return None ``` 형식이 맞지 않을 때 에러를 내뿜는 대신 **`None`을 반환하며 조용히 뒤로 빠집니다.** 이 사소한 처리가 DRF의 유연성을 만듭니다. 덕분에 우리는 `AUTHENTICATION_CLASSES` 리스트에 여러 인증기를 순서대로 담을 수 있습니다. "아, 내 방식이 아니네? 그럼 다음 인증기 확인해봐!"라고 배려하는 개발자들의 흔적이 느껴지지 않나요? --- ## 마무리 {#sec-e57eed9058e2} `BasicAuthentication`은 구식처럼 보이지만, 그 구조만큼은 현대적인 인증 시스템의 근간을 이루고 있습니다. 특히 예외 처리와 다중 인증 지원 방식은 우리가 커스텀 인증기를 만들 때 반드시 흡수해야 할 자산입니다. 자, 이제 첫 번째 산을 넘었습니다. 다음 포스트에서는 우리에게 익숙하면서도 여전히 아리송한 **`SessionAuthentication`**을 파헤쳐 보겠습니다. **"당신이 세션 인증을 쓰면서도 몰랐던 사실들"**, 기대해 주세요!