深入 DRF 原始碼:我曾只寫自訂身份驗證器,直到重新發現「內建身份驗證器」的奧秘

1. 我熱愛 DRF,卻不信任其內建身份驗證器



Django 和 DRF (Django Rest Framework) 是我開發生涯的忠實夥伴。然而,在處理身份驗證 (Authentication) 時,我總有著固執己見的一面。我主要偏好使用 JWTAPI KeyOAuth2 等方式,因為這些在現代應用程式環境中已是標準。

然而,每當我翻閱 DRF 官方文件時,總會發現頁面主要介紹的,卻是像 BasicSessionTokenRemote 這類看似「老舊」的身份驗證類別,而非我慣用的現代方法。我常會疑惑:「如此強大的 DRF,為何要內建這些東西?」因此,我總是繼承 BaseAuthentication,然後建立自己的自訂身份驗證器。

2. 為何要重新檢視「內建身份驗證器」的原始碼?

在開發自訂身份驗證器時,我突然產生了一個想法:

「我所建立的身份驗證器,是否真的完美融入了 Django 和 DRF 的核心哲學?」

我開始思考,除了單純地將使用者資訊放入 request.user 之外,我是否錯失了 DRF 所設計的身份驗證架構中那種「自然而然」的流暢感。因此,從今天起,我將在接下來的幾篇文章中,徹底剖析 DRF 的四種內建身份驗證器:

  • BasicAuthentication (HTTP 基本身份驗證)
  • SessionAuthentication (利用 Django Session)
  • TokenAuthentication (簡單令牌身份驗證)
  • RemoteUserAuthentication (整合外部身份驗證)

3. 在「不適感」背後尋找「哲學」



老實說,在現代服務中直接使用 BasicAuthentication 會讓人感到不自在。它看起來對安全性較為脆弱,每次都傳送使用者名稱和密碼的結構也令人不安。然而,深入分析其原始碼,你會發現其中蘊含著一套精密的設計,關於「當身份驗證失敗時,如何與客戶端溝通」

例如,單純地拋出 403 Forbidden 與透過 authenticate_header 引導標準規範的 401 Unauthorized 之間,存在著顯著的差異。正是這些細節,造就了「Django 風格」的類別。

4. 本系列的目標:讓自訂實作「自然融入」

在本系列結束時,我期望獲得的不僅僅是知識:

  1. 啟發 (Inspiration): 學習內建身份驗證器為何採用這種結構,以及其背後的設計哲學。
  2. 移植 (Porting): 將這些哲學應用到我常用的 JWT 或 OAuth2 自訂身份驗證器中。
  3. 和諧 (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.POSTrequest.data。然而,身份驗證資訊卻是「國際慣例」般地被放置在標頭中。這是為什麼呢?

因為身份驗證必須靈活應對所有 HTTP 方法 (GET, POST, PUT, DELETE 等)GET 請求根本沒有 Body,而 DELETE 通常也缺乏請求體。如果身份驗證資訊必須放在 Body 中,那麼我們在進行查詢時,就不得不被迫使用 POST,這會導致設計變得非常奇怪。將身份驗證資訊放在標頭中,就像是「無論打開哪扇門,都請把身份證拿在手上」的約定。


冷酷的現實:安全漏洞

這種方式最大的缺點在於 Base64。它只是一種「編碼」,而非加密。任何人都能在解碼網站上,一秒鐘內獲取你的使用者名稱和密碼。

結論: 在 HTTP 環境中絕對禁止使用。它只能在 HTTPS 這種安全的通道內,並且僅限於極為有限的用途。


第一個啟發:機器間通訊 (M2M) 的高性價比安全

儘管如此,這個看似老舊的身份驗證器仍有其迷人之處,那就是在伺服器與伺服器之間進行通訊時。當建立 HMAC 或複雜的 API Key 系統會耗費過多資源,但直接傳輸資料又令人不安時,我們可以「稍微」調整這個結構:

  • 自訂加密: 不使用 Base64,而是傳輸雙方伺服器皆知的對稱金鑰加密後的二進制內容。
  • 簡易實作: 無需複雜的握手,僅透過 Basic <Encrypted_Data> 形式,即可實現快速且安全的通訊。這就變成了一個「只有我們知道的密碼」的有效傳輸通道。

第二個啟發:失落的齒輪 authenticate_header()

最值得關注的方法莫過於 authenticate_header()。在原始碼中,你找不到任何直接呼叫它的地方。然而,它卻是 DRF 異常處理器中,產生「401 Unauthorized」的關鍵鑰匙

當我們建立自訂身份驗證器時,常常會忽略這個方法。事實上,即使少了這個方法,身份驗證 (Authentication) 行為本身也不受任何影響。但是,這種微小的差異會讓 View 層的程式碼變得多麼混亂,我們有必要進行比較。

案例 1. 省略 authenticate_header 時 (手動方式)

如果缺少這個方法,DRF 會判斷「無法提供身份驗證方式的指引」,因此在身份驗證失敗時,它會拋出 403 Forbidden 而非 401。為了避免這種情況,我們需要在 View 中進行以下「繁瑣」的操作:

# ❌ 缺少 authenticate_header 的情況
class MyPostAPIView(APIView):
    # 使用 IsAuthenticated 會直接返回 403,為了 401,我們暫時開放權限。
    permission_classes = [AllowAny] 

    def post(self, request):
        # 必須在 View 內部逐一檢查身份驗證狀態,並手動返回 401。
        if not request.user or not request.user.is_authenticated:
            return Response(
                {"detail": "로그인이 필요합니다."}, 
                status=status.HTTP_401_UNAUTHORIZED
            )

        # ... 實際邏輯開始 ...

案例 2. 實作 authenticate_header 時 (DRF 哲學的實踐)

相反地,只要在自訂身份驗證器中加入一行指引 (authenticate_header),DRF 龐大的身份驗證架構就會感知到它,並自動組裝 401 響應。

# ✅ 實作 authenticate_header 的情況
class MyPostAPIView(APIView):
    # 現在 DRF 會自動拋出 401,因此我們可以宣告式地控制權限。
    permission_classes = [IsAuthenticated] 

    def post(self, request):
        # View 只需專注於「業務邏輯」。
        # 身份驗證失敗的處理已在上層 (身份驗證器的 authenticate_header 方法) 完成。
        ... 

這正是「Django 風格」的美學。它避免了將零碎的程式碼散佈在 View 中,而是將我們的程式碼完美地嵌入框架設計的齒輪之間。僅僅透過這個方法,我們就能清晰地分離「身份驗證」與「授權」的職責,並讓 View 保持輕巧。


DRF 的真正魅力:低調的讓步 (None 的藝術)

最後,讓我們再次看看原始碼的這一部分:

if not auth or auth[0].lower() != b'basic':
    return None

當格式不符時,它並沒有拋出錯誤,而是返回 None,悄悄地退到後方。這種微小的處理造就了 DRF 的靈活性。正因如此,我們才能在 AUTHENTICATION_CLASSES 列表中依序放置多個身份驗證器。這是否讓你感受到開發者們的體貼,彷彿在說:「啊,這不是我的方式?那就檢查下一個身份驗證器吧!」


總結

BasicAuthentication 雖然看似過時,但其結構卻是現代身份驗證系統的基礎。特別是其異常處理和多重身份驗證支援方式,是我們在建立自訂身份驗證器時必須學習的寶貴資產。

好了,我們已經跨過了第一座山。在下一篇文章中,我們將深入探討對我們來說既熟悉又有些模糊的 SessionAuthentication

「即使你正在使用 Session 身份驗證,卻可能不知道的那些事」,敬請期待!