FastAPI 认证错误状态码定制指南:通过 `make_not_authenticated_error` 从 401 回退到旧版 403
本文以 FastAPI 0.122.0 引入的认证失败响应变更(403 → 401 + WWW-Authenticate)为切入点,讲解如何通过覆盖安全工具类中的 make_not_authenticated_error 方法,恢复旧版 403 Forbidden 行为以满足存量客户端兼容需求。读完本文你将掌握认证失败异常的产生链路、各类内置安全工具的默认差异,以及如何用十行以内代码完成状态码定制,并了解对应的测试验证方式。
背景:0.122.0 版本对认证失败响应的行为变更
在 FastAPI 0.122.0 版本之前,内置安全工具(HTTPBearer、HTTPBasic、API Key、OAuth2 等)在认证失败并向客户端返回错误时,使用的是 403 Forbidden 这个 HTTP 状态码。
从 FastAPI 0.122.0 版本开始,内置安全工具改用语义上更贴切的 401 Unauthorized 状态码,并在响应中附带合适的 WWW-Authenticate 响应头。这一调整遵循了 HTTP 规范的相关规定:
- 当服务端以
401 Unauthorized拒绝请求时,必须通过WWW-Authenticate头告知客户端应当使用哪种认证方案; 403 Forbidden则更多表示“已识别请求者但拒绝其访问”,与“尚未提供有效凭证”的语义存在偏差。
换言之,新行为更准确地描述了“认证未通过”这一状态,而不是“认证已通过但被禁止访问”。
存量客户端的兼容问题与解决思路
行为变更本身更规范,但如果你有存量客户端依赖旧行为——例如把 403 当作“未登录需跳转登录页”的触发条件、或在后端日志/告警规则里按状态码分流——那么升级 FastAPI 后这些逻辑可能静默失效。
FastAPI 为这种情况保留了明确的定制入口:在自定义安全工具子类中覆盖 make_not_authenticated_error 方法,即可完整接管“认证失败时抛出何种异常”的决策,从而按需返回任意状态码。
完整示例:让 HTTPBearer 认证失败时返回 403
官方示例位于 docs_src/authentication_error_status_code/tutorial001_an_py310.py,它创建了 HTTPBearer 的子类 HTTPBearer403,将默认的 401 覆盖为 403:
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
app = FastAPI()
class HTTPBearer403(HTTPBearer):
def make_not_authenticated_error(self) -> HTTPException:
return HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated"
)
CredentialsDep = Annotated[HTTPAuthorizationCredentials, Depends(HTTPBearer403())]
@app.get("/me")
def read_me(credentials: CredentialsDep):
return {"message": "You are authenticated", "token": credentials.credentials}
这段代码的关键点:
- 继承而非修改:
HTTPBearer403(HTTPBearer)只覆盖认证失败异常的产生逻辑,HTTPBearer原有的凭证解析、OpenAPI 文档生成(securitySchemes中仍是{"type": "http", "scheme": "bearer"})等能力全部保留; - 使用
Depends注入:与直接实例化HTTPBearer()相同,通过Annotated+Depends组合声明依赖,路径操作函数中即可直接取到解析后的HTTPAuthorizationCredentials; - 状态码可自由组合:示例使用
status.HTTP_403_FORBIDDEN,你也可以据此返回status.HTTP_401_UNAUTHORIZED之外的任何自定义语义状态码。
重要提示:返回异常实例而非抛出
官方文档特别强调了一个易错点:
该方法返回的是异常实例,而不是在方法内部抛异常。真正的抛掷动作由内部其余代码完成。
这从示例代码可以印证——方法体内只有 return HTTPException(...),没有任何 raise 语句。之所以这样设计,是为了让框架可以在不同调用路径上统一处理:无论是否启用 auto_error,认证失败都收敛到同一处“生成异常”的逻辑,而“何时抛出”由调用方决定。
源码级原理:认证失败异常是怎么产生的
默认实现位于 HTTPBase 基类
所有 HTTP 认证工具(HTTPBasic、HTTPBearer、HTTPDigest)都继承自 fastapi/security/http.py 中的 HTTPBase。默认的 make_not_authenticated_error 与配套的 make_authenticate_headers 实现在 http.py#L84-L92:
def make_authenticate_headers(self) -> dict[str, str]:
return {"WWW-Authenticate": f"{self.model.scheme.title()}"}
def make_not_authenticated_error(self) -> HTTPException:
return HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers=self.make_authenticate_headers(),
)
默认行为由此一目了然:认证失败时生成一个 401 状态码、"Not authenticated" 详情的 HTTPException,并依据当前认证方案(Bearer、Basic、Digest 等)自动带上 WWW-Authenticate 头。从源码结构看,make_authenticate_headers 被单独拆出,就是为了便于各方案定制挑战头内容。
调用时机:何时触发抛出
以 HTTPBearer 为例,其在 http.py#L303-L316 的 __call__ 方法中解析 Authorization 头:
authorization = request.headers.get("Authorization")
scheme, credentials = get_authorization_scheme_param(authorization)
if not (authorization and scheme and credentials):
if self.auto_error:
raise self.make_not_authenticated_error()
else:
return None
可见触发路径集中在两类情况:
- 凭证缺失或格式不完整(无
Authorization头、无 scheme、无凭证); - scheme 不匹配(如声明了
Bearer却收到了其他方案,http.py#L311-L315)。
HTTPBasic 还会在 base64 解码失败、缺少分隔冒号等场景下抛出同一异常(见 http.py#L212-L218)。
auto_error=False 的旁证
上述代码还揭示了一个细节:当 auto_error=False 时不会抛出异常而是返回 None。由于“生成异常”与“抛出异常”被解耦,这个开关才能干净地实现可选认证——这也正是 make_not_authenticated_error 只返回不抛出的架构价值所在。
其他内置安全工具的差异化实现
并非所有安全工具都共享 HTTPBase 的默认实现,了解它们各自的实现能帮你举一反三:
| 安全工具 | 源码位置 | 认证失败响应细节 |
|---|---|---|
HTTPBasic |
fastapi/security/http.py | 覆盖 make_authenticate_headers,若配置了 realm 则返回 Basic realm="..."(http.py#L197-L200) |
APIKeyBase(API Key) |
fastapi/security/api_key.py | 401 + WWW-Authenticate: APIKey。因 HTTP 规范要求 401 必须携带该头,而 API Key 认证暂无标准挑战方案,故使用自定义挑战值 APIKey(api_key.py#L31-L45) |
OAuth2(Bearer 流程) |
fastapi/security/oauth2.py | 401 + WWW-Authenticate: Bearer(oauth2.py#L401-L421) |
OpenIdConnect |
fastapi/security/open_id_connect_url.py | 401 + WWW-Authenticate: Bearer(open_id_connect_url.py#L80-L85) |
从这些实现可以看出 make_not_authenticated_error 是统一而稳定的扩展点:任何继承自这些基类的子类,只需重写这一个方法即可整体改变认证失败响应的状态码与响应头,同时保留各方案自身的解析与挑战头逻辑。
测试验证:行为与 OpenAPI 双重校验
仓库中配套了针对该示例的测试用例 tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py,包含三个维度的断言:
- 认证成功路径:携带
Authorization: Bearer secrettoken请求/me应返回200及解析出的 token(对应示例中credentials.credentials的取值逻辑); - 认证失败路径:不带凭证请求
/me应返回403与{"detail": "Not authenticated"},精确验证了子类覆盖生效; - OpenAPI 文档完整性:
/openapi.json中/me的security声明引用了HTTPBearer403,且securitySchemes中该方案被识别为{"type": "http", "scheme": "bearer"}。
第三点很有价值:它证明覆盖认证失败状态码不会破坏 OpenAPI 文档生成。即使类名变成了 HTTPBearer403,只要继承关系正确,FastAPI 依然能基于 HTTPBase 的模型信息自动生成正确的 Bearer 安全方案描述,交互式文档中的“Authorize”功能不受影响。
迁移与定制建议
- 升级前先评估客户端:确认是否有代码、网关或监控把
403硬编码为“认证失败”信号;若有,优先让客户端适配401(新行为符合 HTTP 规范),实在无法调整时再使用本文方案回退; - 按安全方案分别定制:本文示例针对
HTTPBearer,如果同时使用 API Key、OAuth2 等方案,需要对每种方案的类分别创建子类并覆盖对应方法; - 注意 403 不应携带
WWW-Authenticate:回退到403时示例直接省略了headers参数,这是合理的——WWW-Authenticate专属于401的挑战语义,挂在403上反而会造成语义混淆; - 保持 detail 一致:示例保留
"Not authenticated"默认文案,便于存量客户端按该消息继续识别失败原因,也可按业务需要改为自己的描述。
综上所述,make_not_authenticated_error 是 FastAPI 为安全工具开放的一个小而精的扩展点:它把“认证失败后客户端应看到什么”完整交给开发者,在向后兼容与规范演进之间提供了干净的取舍空间,且几乎不影响类型安全、依赖注入与 OpenAPI 生态。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00