DRF 源码之旅:我曾只用自定义认证器,直到重新发现内置认证器的魅力

1. 我热爱 DRF,但曾不信任它的内置认证器。



Django 和 DRF (Django Rest Framework) 是我开发生涯中不可或缺的伙伴。然而,在认证(Authentication)方面,我一直有着自己的“执念”。我通常偏爱 JWTAPI KeyOAuth2 方案,因为它们是现代应用程序环境下的标准。

奇怪的是,当我翻阅 DRF 官方文档时,发现占据主体的并非我常用的现代认证方式,而是 Basic、Session、Token、Remote 等看起来有些“老旧”的认证类。我常常疑惑:“功能强大的 DRF 为何还要内置这些?”因此,我总是习惯性地继承 BaseAuthentication,开发并使用自己的自定义认证器。

2. 为何重新审视“内置认证器”的源码?

在不断开发自定义认证器的过程中,我突然产生了一个疑问:

“我所构建的认证器,是否真的完美契合 Django 和 DRF 的设计哲学?”

我开始思考,除了简单地将用户对象放入 request.user 之外,我是否错过了 DRF 所设计的认证方案中那种“浑然天成”的精妙之处。因此,从今天开始,我将分几篇文章,彻底剖析 DRF 的四种内置认证器:

  • BasicAuthentication (HTTP 基本认证)
  • SessionAuthentication (利用 Django 会话)
  • 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 也常为空。如果认证信息必须放在 Body 中,那么即使只是查询数据,我们也不得不强行使用 POST 请求,这将导致设计变得非常奇怪。将认证信息放入请求头,就像是约定好“无论打开哪扇门,都请把身份证拿在手上”


冷酷的现实:安全漏洞

这种方式最大的缺点是 Base64。它仅仅是“编码”而非加密。任何人只需一秒钟,通过在线解码工具就能获取你的用户名和密码。

结论: 在 HTTP 环境下绝对禁止使用。只能在 HTTPS 这种安全的隧道中,并且仅限于非常有限的用途。


第一个启发:机器间通信 (M2M) 的高性价比安全

尽管如此,这种看似过时的认证器也有其魅力之处,那就是在服务器之间进行通信时。 HMAC 或复杂的 API Key 系统构建成本较高,但又不想直接裸传数据时,我们可以“巧妙”地调整这种结构:

  • 自定义加密: 不使用 Base64,而是传输经过双方服务器才知晓的对称密钥加密的二进制内容。
  • 简便实现: 无需复杂的握手,仅通过 Basic <Encrypted_Data> 形式就能实现快速且安全的通信。它成为了“只有我们之间才知道的密码”的高效传输通道。

第二个启发:被遗忘的齿轮 authenticate_header()

最值得关注的方法是 authenticate_header()。在源代码中,你找不到任何直接调用它的地方。然而,它却是 DRF 异常处理器中生成“401 Unauthorized”响应的核心钥匙

在创建自定义认证器时,我们经常会忽略这个方法。实际上,即使缺少这个方法,对认证(Authentication)行为本身也没有任何影响。但有必要比较一下,这种微小的差异会使视图层的代码变得多么混乱。

情况 1. 忽略 authenticate_header 时(手动方式)

如果缺少这个方法,DRF 会认为“无法提供认证方式的指导”,因此在认证失败时会抛出 403 Forbidden 而非 401。为了避免这种情况,视图中需要进行如下“繁琐”的操作:

# ❌ 缺少 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
            )

        # ... 实际业务逻辑开始 ...

情况 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

敬请期待“你可能不知道的 Session 认证秘密”