FastAPI 认证错误状态码迁移指南:将 401 恢复为旧版 403 的兼容方案
自 FastAPI 0.122.0 起,内置安全工具(security utilities)在认证失败后返回的错误状态码从 403 Forbidden 调整为更符合 HTTP 规范的 401 Unauthorized,并附带标准的 WWW-Authenticate 响应头。本文面向依赖旧行为(403)的存量客户端,介绍如何在当前 FastAPI 仓库中通过重写 make_not_authenticated_error 方法,让 HTTPBearer 等安全类精确恢复 403 行为,同时说明其底层实现与测试验证路径。
背景:认证失败该返回 401 还是 403?
HTTP 规范对两者有明确分工:401 Unauthorized 表示"请求尚未经过身份验证,或未提供有效凭证",且响应必须携带 WWW-Authenticate 头以告知客户端如何发起挑战(challenge);而 403 Forbidden 表示"服务器已理解请求,但拒绝执行",通常用于已认证但无权限的场景。相关定义见 RFC 7235 与 RFC 9110 中关于 401 Unauthorized 的章节。
FastAPI 0.122.0 之前,内置安全工具在认证失败后向客户端返回 403 Forbidden。从 0.122.0 开始,它们改用更合适的 401 Unauthorized,并依据 HTTP 规范在响应中返回恰当的 WWW-Authenticate 头。
然而,如果客户端的既有逻辑依赖旧的 403 行为(例如前端或第三方集成把 403 当作"请跳转登录页"的硬编码判断),升级 FastAPI 后可能引发回归。此时不需要改动全局异常处理,只需要在自己的安全类中重写 make_not_authenticated_error 方法即可恢复旧行为。
迁移到 401 的源码依据
从源码结构看,这一改动是通过统一提取"构造未认证错误"的钩子方法实现的。所有内置安全类都在各自的基类中实现了 make_not_authenticated_error,并在检测到认证缺失或凭证非法时通过 raise 抛出它返回的异常实例:
- fastapi/security/http.py:
HTTPBase中的默认实现返回status_code=HTTP_401_UNAUTHORIZED、detail="Not authenticated",并通过make_authenticate_headers()附带形如WWW-Authenticate: Bearer的头; - fastapi/security/api_key.py:
APIKeyBase同样返回401,由于 API Key 没有标准化的挑战值,实现中发送了自定义挑战WWW-Authenticate: APIKey(其注释明确引用了 RFC 9110 对 401 必须携带该头的要求); - fastapi/security/oauth2.py:
OAuth2基类默认返回401与WWW-Authenticate: Bearer,注释说明 OAuth2 规范未定义固定挑战值,出于实用考虑采用最常见的Bearer挑战。
在 HTTPBasic、HTTPBearer、HTTPDigest 的 __call__ 实现中,凡是凭证缺失、scheme 不匹配或解码失败的路径,都会调用 raise self.make_not_authenticated_error()(例如 fastapi/security/http.py 与 fastapi/security/http.py)。这正是该钩子方法能被安全子类覆盖以实现状态码定制的关键:无论认证过程有多少失败分支,最终生成的异常都出自同一个方法。
重写 make_not_authenticated_error 恢复 403
在 FastAPI 0.122.0+ 中,若需恢复旧版 403 Forbidden 行为,可在继承 HTTPBearer(或其他安全类)后重写该方法。官方指南 authentication-error-status-code 中给出完整示例,仓库中的可执行源码位于 docs_src/authentication_error_status_code/tutorial001_an_py310.py,全文如下:
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仅重写了make_not_authenticated_error,其余认证与 OpenAPI 集成逻辑完全继承自父类,因此 OpenAPI 文档中该安全方案(名为HTTPBearer403)依然是标准的http+bearerscheme; - 重写方法返回构造好的
HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated"),注意这里不再附带WWW-Authenticate头——这与403语义一致,因为只有401才被要求携带挑战头; - 可复用
status模块中预定义常量,如status.HTTP_403_FORBIDDEN,避免硬编码魔法数字; CredentialsDep通过Annotated类型别名将安全依赖与类型提示绑定,供路径操作函数直接使用。
官方文档特意强调一个易错点:该方法返回的是异常实例(return),而不是抛出它(raise)。真正把异常抛出的动作发生在 FastAPI 内部安全依赖的执行代码中,若在此处直接 raise,会导致异常提前离开安全类上下文的正常调用路径,破坏依赖注入流程。
运行验证与测试用例佐证
仓库为上述教程源码提供了端到端测试 tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py,可用它验证改造后的真实行为:
# 携带合法 Bearer 凭证访问 -> 200
response = client.get("/me", headers={"Authorization": "Bearer secrettoken"})
assert response.status_code == 200
assert response.json() == {
"message": "You are authenticated",
"token": "secrettoken",
}
# 不携带任何凭证访问 -> 403(即恢复后的旧行为)
response = client.get("/me")
assert response.status_code == 403
assert response.json() == {"detail": "Not authenticated"}
该测试同时校验了 /openapi.json:安全方案名沿用子类类名 HTTPBearer403,OpenAPI 声明为 {"type": "http", "scheme": "bearer"},与父类 HTTPBearer 一致——说明重写错误状态码不会破坏自动生成的接口文档。测试还通过 snapshot 固化了 OpenAPI schema,防止后续改动意外变更文档结构。
如何验证新旧行为的差异
若本地环境已升级到含该改动的 FastAPI 版本,可分别构造两次无凭证请求直观对比:
- 默认
HTTPBearer:响应状态码为401,响应体为{"detail": "Not authenticated"},且响应头携带WWW-Authenticate: Bearer; - 重写后的
HTTPBearer403:响应状态码为403,响应体同为{"detail": "Not authenticated"},但不携带WWW-Authenticate头。
这解释了迁移的完整含义:不仅是状态码变化,还新增了符合规范的挑战头。旧客户端若只判断 403 而不解析 WWW-Authenticate,升级后必然受影响,此时按本文方案做最小化定制即可平滑过渡。
迁移注意事项
- 影响面:
make_not_authenticated_error同时存在于 HTTP(Basic/Bearer/Digest)、API Key(Query/Header/Cookie)与 OAuth2(OAuth2PasswordBearer、OAuth2AuthorizationCodeBearer等)各类的基类中,本方案对所有内置安全类型通用,无需为每个子类重复实现;若项目使用自定义安全方案或非 Bearer 的 OAuth2 流程,可参考 fastapi/security/oauth2.py 中注释建议按需重写挑战值。 - 临时兼容而非长期方案:
403用于"已识别但拒绝",401用于"未认证",按规范语义应使用401并携带WWW-Authenticate引导客户端补全凭证。恢复403仅是存量客户端的过渡手段,建议同步推动客户端适配新行为。 - 保持 OpenAPI 一致性:重写只影响运行时错误响应,不影响
/openapi.json与/docs中的安全 scheme 声明(见上文 schema 断言),因此无文档漂移风险。
若需要同时支持"可选认证"(允许匿名访问时返回 None 而非报错),仍可配合安全类自带的 auto_error=False 参数使用;auto_error 控制失败分支走 raise 还是返回 None,而 make_not_authenticated_error 只负责定制真正报错时的异常内容,两者职责独立,可在 fastapi/security/http.py 的 HTTPBearer 实现中交叉印证。
相关参考文档见 docs/hi/docs/how-to/authentication-error-status-code.md(本文对应翻译)与官方英文版 authentication-error-status-code。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python08
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00