
FastAPI 安全工具鉴权失败状态码用make_not_authenticated_error把 401 回退成 403 的兼容方案【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi本文面向需要与旧客户端保持兼容的 FastAPI 开发者讲解0.122.0版本后内置安全工具Security Utilities默认鉴权错误从403 Forbidden变为401 Unauthorized并附带WWW-Authenticate响应头的来龙去脉并结合源码说明如何通过覆写make_not_authenticated_error方法精确回退旧行为。读完你可以复现官方示例、理解覆写点与抛错点的分工并针对HTTPBearer、HTTPBasic、API Key、OAuth2 等各类安全工具自行定制鉴权失败响应。一、变更背景为什么从 403 改成 401根据仓库中对应的英文文档 docs/en/docs/how-to/authentication-error-status-code.md 与本文所依据的葡萄牙语文档 docs/pt/docs/how-to/authentication-error-status-code.md 的说明在 FastAPI0.122.0之前内置安全工具HTTPBasic、HTTPBearer、HTTPDigest、APIKey*、OAuth2*、OpenIdConnect等在鉴权失败并需要向客户端返回错误时使用的 HTTP 状态码是403 Forbidden。从0.122.0开始它们改用了语义上更准确的401 Unauthorized并在响应中返回合理的WWW-Authenticate响应头使其符合 HTTP 规范RFC 7235 第 3.1 节、RFC 9110 中关于401 Unauthorized的规定。这两处变更的语义差异值得注意403表示“资源存在但你无权访问”而401表示“你还没有提供或提供了无效的认证凭据”。对于“请求未携带凭证”这一情形401明显更贴合 HTTP 语义——这也是新版本默认行为的改进点。说明RFC 规范属于背景性公开资料本文不展开外部链接仅转述其与401WWW-Authenticate的对应关系。二、新默认行为在源码中的落点要理解“如何回退”首先得知道新行为是在哪里、以什么方式实现的。仓库源码 fastapi/security/http.py 中所有 HTTP 类安全工具HTTPBasic、HTTPBearer、HTTPDigest的公共基类HTTPBase提供了两个关键方法# fastapi/security/http.pyHTTPBase 内 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_codeHTTP_401_UNAUTHORIZED, detailNot authenticated, headersself.make_authenticate_headers(), )可以看到默认逻辑非常直观fastapi/security/http.py#L84-L92make_authenticate_headers()依据安全方案的名称生成WWW-Authenticate头例如 Bearer 方案的值为Bearermake_not_authenticated_error()返回一个status_code401、detailNot authenticated、且携带WWW-Authenticate响应头的HTTPException实例。而HTTPBasic还会针对 realm认证域覆写make_authenticate_headers()生成Basic realm...形式的值见 fastapi/security/http.py#L197-L200。2.1 各安全工具默认实现一览从源码检索可以确认make_not_authenticated_error是所有内置安全工具公用的“错误工厂方法”各子类按需定制响应头安全类实现位置默认 401 时携带的WWW-AuthenticateHTTPBase及其子类HTTPBasic/HTTPBearer/HTTPDigestfastapi/security/http.py#L87-L92Bearer、Basic等由make_authenticate_headers()生成APIKeyBaseAPIKeyHeader/APIKeyQuery/APIKeyCookiefastapi/security/api_key.py#L31-L45自定义挑战值APIKeyRFC 未为 API Key 标准化挑战头但 401 必须携带该头故用APIKeyOAuth2含OAuth2PasswordBearer等fastapi/security/oauth2.py#L401-L421Bearer代码注释说明 OAuth2 规范未定义挑战方式出于实用考量默认用 BearerOpenIdConnectfastapi/security/open_id_connect_url.py#L80同样基于 401 默认路径例如 API Key 的实现就为 401 响应附上了headers{WWW-Authenticate: APIKey}源码注释还专门解释了原因HTTP 规范要求401响应必须包含WWW-Authenticate头而 API Key 没有标准化挑战因此发送自定义值APIKeyfastapi/security/api_key.py#L31-L45。2.2 抛错发生在调用方而非工厂方法关键点在于make_not_authenticated_error只负责“构造并返回异常实例”真正“抛出”异常的是各个安全工具的__call__协程内部。以HTTPBearer为例fastapi/security/http.py#L303-L316async def __call__(self, request: Request) - HTTPAuthorizationCredentials | None: 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 if scheme.lower() ! bearer: if self.auto_error: raise self.make_not_authenticated_error() else: return None return HTTPAuthorizationCredentials(schemescheme, credentialscredentials)从代码结构可以看出两层设计错误构造与错误抛出分离所有判断分支都只调用raise self.make_not_authenticated_error()抛出的正是工厂方法返回的同一个实例受auto_error开关控制当构造安全工具实例时传入auto_errorFalse用于可选认证所有分支都不会抛错而是返回None。这意味着覆写make_not_authenticated_error只影响“决定抛错”的那部分行为不会影响可选认证的None返回路径。三、何时需要回退到 403 旧行为虽然401语义上更正确但在实际工程中你仍可能遇到必须维持403的场景例如已有客户端App、网关、SDK把403硬编码为“需要重新登录”的触发条件旧版服务端日志、监控告警规则依赖403状态码分类鉴权失败中间链路代理或 WAF 对401与403有不同的拦截策略。文档明确给出的结论是如果由于某种原因你的客户端依赖旧行为可以通过在自己的安全类中覆写make_not_authenticated_error方法恢复旧行为。四、完整示例定制返回 403 的 HTTPBearer官方仓库在 docs_src/authentication_error_status_code/tutorial001_an_py310.py 提供了一个可直接运行的完整示例核心思路是创建HTTPBearer的子类HTTPBearer403覆写make_not_authenticated_error让它返回403 Forbiddenfrom 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_codestatus.HTTP_403_FORBIDDEN, detailNot 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主要做了三处改动状态码改为status.HTTP_403_FORBIDDENdetail沿用默认的Not authenticated也可换成你自己的文案不再携带WWW-Authenticate响应头因为403并不要求该头这完全符合旧版行为。运行后访问/me且不提供Authorization头时客户端将收到403 Forbidden携带合法 Bearer 凭证如Authorization: Bearer token则正常返回{message: You are authenticated, token: ...}。4.1 方法返回异常实例而不是抛出它请务必注意官方文档特别强调的一个细节覆写的方法返回的是异常实例return HTTPException(...)而不是在方法内部直接raise。原因正如上文 2.2 节所示抛出动作统一由__call__内部其余代码完成raise self.make_not_authenticated_error()。如果你的覆写方法里多写了一个raise异常会在错误工厂内部被提前抛出破坏统一构造/统一抛出的约定导致行为不可预期。4.2 其他安全类的定制方式完全一致由于make_not_authenticated_error是贯穿所有安全工具的统一点同样的“子类 覆写”套路适用于任意内置类例如想让HTTPBasic鉴权失败返回403class HTTPBasic403(HTTPBasic): ...想让APIKeyHeader鉴权失败返回403class APIKeyHeader403(APIKeyHeader): ...想让OAuth2PasswordBearer鉴权失败返回403class OAuth2PasswordBearer403(OAuth2PasswordBearer): ...。每个子类内部只需复制上面make_not_authenticated_error的403实现即可。五、验证方案与测试依据仓库自身的测试 tests/test_security_http_bearer.py 明确断言了新默认行为缺失凭证时状态码为401且响应头中WWW-Authenticate的值为Bearer见该文件中围绕401与WWW-Authenticate Bearer的多组断言。你可以把它当作对照基线来理解默认行为再为自己的HTTPBearer403编写反向验证。动手验证自定义子类时可以构造如下用例思路用TestClient向/me发起不带Authorization头的请求断言response.status_code 403断言响应 JSON 中detail Not authenticated同时可断言响应头中不再出现WWW-Authenticate与旧行为一致。如果你希望保留“两种状态码共存”的优雅切换方案还可以让子类通过构造函数参数控制返回401还是403而不必写死状态码。六、注意事项与小贴士覆写并不改变 OpenAPI 文档中的安全方案覆写只影响运行时鉴权失败的响应/docs中生成的 OpenAPI 安全描述securitySchemes仍由scheme_name与模型决定。不过从源码可见HTTPBearer.__init__会以scheme_name or self.__class__.__name__作为方案名fastapi/security/http.py#L299-L300因此子类名称HTTPBearer403会默认成为 OpenAPI 中的 scheme 名称——如需维持原名可在实例化时显式传入scheme_name。auto_errorFalse时不受影响当安全工具以auto_errorFalse构造可选认证场景时所有分支走return None根本不会调用错误工厂方法。保持返回值类型一致方法签名返回HTTPException覆写时请继续返回HTTPException实例或子类确保__call__中的raise语句类型安全。在应用中切换遵循的版本本文基于当前仓库代码默认401WWW-Authenticate展开回退方案即为文档所给的覆写路径可用于整体升级到0.122.0后仍需兼容旧客户端的情况。如果你只想要一个不改变其他任何行为的“旧版兼容层”把HTTPBearer403这类子类放到应用的security模块集中管理并通过Depends统一注入即可改动范围小、可维护性好。【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考