## 1. “登录了,为什么不认识我?”(问题的开端) {#sec-ffb74987f051} OAuth2、JWT、会话认证… 认证方式实在太多了,大多数情况下已经足够。我也曾如此。 * 当在自己写的邮件客户端或 ChatGPT 的 MyGPT 中加入 **OAuth2** 时,感觉“这才是真正的用户体验”。 * 单纯的 Django 服务器的 Web 应用,**会话认证** 是最佳选择! * 前后端分离的项目中,**JWT** 最为简洁。 然而,这些组合在某个时刻会一起崩塌。 关键点是 **异步任务(Celery)**。用户点击按钮时,后端并不是直接处理,而是把任务交给远程的 AI 计算服务器或 worker。此时 worker 会说: > “我收到了请求,但这是谁的请求?没有 request.user”。 ![机器人 worker 手持 api key 递交信件的图片](/media/editor_temp/6/b63803ff-21ba-4f1b-a5af-47c8ca0fdd25.png) ## 2. 问题在于 “后端 ↔ 后端” + “异步 worker(Celery)” {#sec-a7eab2678145} 我决定引入 API Key 的根本原因是 **后端服务器之间的通信**,而 **Celery worker** 介入其中的结构。 用户点击按钮后,请求并不会直接到达 AI 计算服务器,而是: 1. 用户发送 Web 请求 2. 后端把“任务”放入队列 3. Celery worker 消费队列,并 **向某个后端/计算服务器发起异步请求** 在这种情况下,最痛的点是: * worker 没有 **request.user** * 没有会话(因为不是浏览器) * JWT 也很尴尬(谁来管理、在哪里、如何传递 token) * OAuth2 更是需要用户交互,根本不可用 当 JWT 和会话失效时,唯一剩下的问题被压缩为: > “计算服务器如何知道是哪个客户(租户/用户)在执行这项任务?” ## 3. 在 worker 世界里,先要“识别”再谈“认证” {#sec-77f5863a0a93} 在 Web 请求中,**认证 = 登录**,**登录 = 用户**,自然衔接。 但 worker 不是人,而是 **自行借用 CPU 运行的应用程序**,因此在它们之间,先要“识别”。认证可以通过服务器间的 HMAC 或 secretKey 解决。 * 任务必须使用 **A 客户** 的数据执行 * 结果必须保存到 **A 客户** 的资源中 * 计费/权限/配额也要基于 **A 客户** 扣除 如果硬要用 JWT 或会话去套,这会导致 token 的颁发、存储、传递、过期、刷新设计膨胀,而且会产生一种强烈的违和感: > “即使是我的服务器的客户,但用户并未在浏览器中操作,后端去为其获取 JWT?” 这种想法太不自然,我立刻放弃了。 ## 4. 解决方案:API Key 在此环节既简单又强大 {#sec-2110738d8481} 于是我采用了 **API Key**,它一次性解决了所有问题。 * worker 发起内部请求时,只需要 **一个 Header** 即可完成 **认证+识别** * 服务器凭此 key 能立即 **映射到具体的用户/客户** * Key 的回收/更换(轮转)也非常明确 示例请求如下: ```http POST /v1/ai/jobs Authorization: Api-Key Content-Type: application/json { "job_id": "...", "payload": {...} } ``` 即使 worker 没有 `request.user`,只要把上面的请求发出去,接收方后端即可通过文中说明的方式利用 API Key 确认用户身份。 ## 5. 关键改进:把 API Key 与 USER 绑定后运维变得轻松 {#sec-762242363071} 我特别满意的是这点: 常见的 `rest_framework_api_key` 库提供了基本的 Key 功能,但我的场景需要 **“Key ↔ 用户(客户)绑定”**。于是我继承 `AbstractAPIKey`,实现 `CustomAPIKey`,并通过 **FK 关联到 AUTH_USER 模型**。 效果令人惊喜。 ```python from django.conf import settings from rest_framework_api_key.models import AbstractAPIKey from django.db import models class CustomAPIKey(AbstractAPIKey): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="api_keys" ) is_test = models.BooleanField(default=False) # 区分阶段/测试 Key ``` 这样一来,除了普通的 **认证**,还能打开一系列 **运维功能**。 ## 6. 为每个用户自动发放 Key 带来的运维收益 {#sec-d180d12a94f2} 在用户注册时自动为其创建一个 Key,运营上获得了以下便利: ### 1) 便于管理有效性 {#sec-bfe883fb0c50} * 查询/禁用/删除某用户的 Key 非常简单 * 用户注销时,关联的 Key 会随之级联删除 ### 2) Key 轮转更容易 {#sec-7f4bccda836d} * 遇到“Key 泄漏”时,直接生成新 Key 并废除旧 Key,流程清晰 * 支持多 Key,可实现 **无缝切换**(新 Key 部署后再废除旧 Key) ### 3) 计费/配额/权限可以以用户为单位 {#sec-1081bea2890b} * 不再是基于单个 Key,而是 **基于用户** 进行计费和限制 * 不需要每次都去推断 “这个 API Key 属于谁” ### 4) 单个用户可拥有多个 Key {#sec-1bc8fa524379} `is_test` 之类的标记在这里发挥作用。因为 API Key 通过 FK 关联,而不是 OneToOne,单个用户可以拥有多把钥匙用于不同场景。 * 同一用户可以同时拥有 **测试环境 Key** 与 **生产环境 Key** * 开发/运维流程可以清晰分离 * 在日志/监控中也能轻松区分 “测试流量” 与 “真实流量” ## 7. 认证方式不是优劣之分,而是“情境武器选择” {#sec-f73d5faf76f6} 总结我目前的最佳组合如下: * **OAuth2**:适用于外部服务/客户端集成、需要用户授权的场景 * **会话认证**:单体 Django Web 应用,开发速度快、实现最简 * **JWT**:前后端分离、移动端/SPA 等多客户端场景的平衡方案 * **API Key**:后端‑后端、自动化/worker/批处理等 **非用户请求** 场景的首选 尤其在 Celery worker 介入时,试图用 **登录认证** 统一整个体系只会增加复杂度。此时 API Key 成为最干净的出路。 ## 8. 结语 {#sec-d746e56908e8} 人(浏览器/App)自然使用会话/JWT/OAuth2 来处理。 但 worker 不是人,而是进程,必须能够 **识别出它代表的是哪个用户的工作**。 我转向 API Key 并不是因为宏大的安全论证,而是因为它在那个环节最简洁地解决了问题。把 Key 与 USER 绑定后,Key 管理不再是工具,而是 **运维杠杆**。 大家在项目中也经常使用 API‑Key 吗?我分享的方式只是 API‑Key 便利性的一个切面,希望能为阅读此文的你带来一点灵感。 --- **相关链接** - [在 Django/DRF 中使用 HMAC 签名保障服务器‑服务器请求的完整性](/ko/whitedec/2025/12/9/django-drf-hmac-signature-server-to-server-integrity/) - [React RCE 事件的教训:HMAC 签名·Key 轮转·零信任的必要性](/ko/whitedec/2025/12/8/react-rce-lesson-hmac-key-rotation-zero-trust/)