ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FastAPI HTTP Basic 认证实战:HTTPBasic 依赖、凭据校验与防时序攻击

FastAPI HTTP Basic 认证实战:HTTPBasic 依赖、凭据校验与防时序攻击 FastAPI HTTP Basic 认证实战HTTPBasic 依赖、凭据校验与防时序攻击【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi本文以 FastAPI 官方的 HTTP Basic 认证文档为核心完整讲解如何用HTTPBasic依赖实现浏览器原生的用户名/密码登录提示、如何用secrets.compare_digest()抵御时序攻击、以及如何正确返回 401 错误触发浏览器重新提示。结合仓库源码 fastapi/security/http.py 与配套测试本文还将深入HTTPBasic的构造参数、Base64 解析流程及 OpenAPI 安全方案生成逻辑帮助你既会用、也懂其底层实现。HTTP Basic 认证的工作机制在最简单的场景下FastAPI 应用可以使用标准的 HTTP Basic 认证应用期待客户端在Authorization请求头中携带用户名和密码如果收不到合法的认证头应用返回 HTTP401 Unauthorized错误响应中附带值为Basic的WWW-Authenticate头可携带可选的realm参数看到这个响应头后浏览器会弹出其内置的用户名/密码输入框用户输入后浏览器会自动将凭据以Authorization: Basic base64(username:password)的形式附加到后续请求中发送。也就是说认证协议本身Base64 编码、WWW-Authenticate头、浏览器弹窗完全由 HTTP 规范和客户端浏览器负责FastAPI 要做的只有两件事解析Authorization头和在失败时正确响应。最简单的 HTTP Basic 认证实现步骤只有四步导入HTTPBasic与HTTPBasicCredentials用HTTPBasic创建一个「security 方案」实例将该security实例作为依赖注入 path operation依赖返回一个HTTPBasicCredentials对象其中包含请求携带的username与password。完整示例对应仓库中的 tutorial006_an_py310.pyfrom typing import Annotated from fastapi import Depends, FastAPI from fastapi.security import HTTPBasic, HTTPBasicCredentials app FastAPI() security HTTPBasic() app.get(/users/me) def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]): return {username: credentials.username, password: credentials.password}第一次打开该 URL 时或在文档界面点击 “Execute” 时浏览器就会要求输入用户名和密码成功认证后直接返回解码出的凭据。这个例子虽然简单但它演示了 FastAPI 认证体系的标准接法security 方案实例化一次然后通过Depends()复用。运行该应用后/openapi.json中会自动生成securitySchemes: {HTTPBasic: {type: http, scheme: basic}}Swagger UI 的 “Authorize” 按钮也随之可用——这一点在测试 tests/test_tutorial/test_security/test_tutorial007.py 的test_openapi_schema中有快照级验证。完整示例校验用户名和密码真实场景下不能只“读取”凭据还必须“校验”凭据是否正确。FastAPI 文档给出的完整示例对应 tutorial007_an_py310.py用一个依赖函数完成校验import secrets from typing import Annotated from fastapi import Depends, FastAPI, HTTPException, status from fastapi.security import HTTPBasic, HTTPBasicCredentials app FastAPI() security HTTPBasic() def get_current_username( credentials: Annotated[HTTPBasicCredentials, Depends(security)], ): current_username_bytes credentials.username.encode(utf8) correct_username_bytes bstanleyjobson is_correct_username secrets.compare_digest( current_username_bytes, correct_username_bytes ) current_password_bytes credentials.password.encode(utf8) correct_password_bytes bswordfish is_correct_password secrets.compare_digest( current_password_bytes, correct_password_bytes ) if not (is_correct_username and is_correct_password): raise HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detailIncorrect username or password, headers{WWW-Authenticate: Basic}, ) return credentials.username app.get(/users/me) def read_current_user(username: Annotated[str, Depends(get_current_username)]): return {username: username}这里的校验逻辑等价于下面这段朴素写法if not (credentials.username stanleyjobson) or not (credentials.password swordfish): # 返回某种错误 ...但关键差异在于使用了 Python 标准库secrets的secrets.compare_digest()带来两层影响字符集约束secrets.compare_digest()只接受bytes或仅包含 ASCII 字符的str。因此像Sebastián这样含á的用户名不能直接传入。示例的解法是先encode(utf8)转成bytes再比较——这也是所有含非 ASCII 凭据的 Basic 认证实现的通用做法时序安全compare_digest是恒定时间比较函数可抵御下文详述的时序攻击。时序攻击Timing Attack是什么设想攻击者正在猜测用户名和密码。第一轮尝试攻击者用johndoe/love123发请求此时应用内的等价代码是if johndoe stanleyjobson and love123 swordfish: ...Python 的字符串比较是“逐字符短路”的johndoe的第一个字符j与stanleyjobson的第一个字符s不同比较立即返回False不会浪费算力去比较剩余字符。应用随即返回“用户名或密码错误”。第二轮尝试攻击者改用stanleyjobsox/love123if stanleyjobsox stanleyjobson and love123 swordfish: ...这次 Python 需要把stanleyjobso这前 12 个字符全部比完才发现末尾不同。因此这一次“用户名或密码错误”的响应多花了几个微秒。响应时间会帮助攻击者如果攻击者发现某次请求的响应时间明显更长他就能推断自己猜得更接近正确答案——即开头的若干字符猜对了。于是他下一次可以尝试更接近stanleyjobsox的组合而不是随便换johndoe这种完全不相干的值。“专业”的自动化攻击现实中攻击者当然不会手工操作。他们会写程序以每秒数千到数百万次的频率发起测试每次只修正一个字符。这样在几分钟到几小时内仅靠响应时间的细微差异攻击者就能把正确的用户名和密码“猜”出来——而服务器本身从未泄露任何凭据明文。用secrets.compare_digest()化解compare_digest保证完整遍历两个输入的每一位才返回结果因此比较stanleyjobsox与stanleyjobson所花的时间和比较johndoe与stanleyjobson所花的时间相同。密码比较同理。只要在认证代码中始终使用secrets.compare_digest()这一类基于响应时间的侧信道攻击就失去了立足点。认证失败时如何返回错误当检测到凭据不正确时示例抛出HTTPException状态码使用401与完全未提供凭据时的状态码一致响应头显式加入{WWW-Authenticate: Basic}这正是让浏览器再次弹出登录提示框的关键——缺少该头浏览器不会主动重新请求认证。测试用例 tests/test_tutorial/test_security/test_tutorial007.py 对该行为做了全路径验证用户名错误alice/swordfish和密码错误stanleyjobson/wrongpassword都会得到401{detail: Incorrect username or password}WWW-Authenticate: Basic头。源码纵深HTTPBasic 是如何工作的HTTPBasic的实现位于 fastapi/security/http.py继承自同文件的HTTPBasefastapi/security/http.py#L69-L102。构造参数HTTPBasic.__init__接受四个关键字参数见 fastapi/security/http.py#L140-L195参数默认值作用scheme_nameNone回退为类名HTTPBasic安全方案名会写入生成的 OpenAPI在/docs中可见realmNoneHTTP Basic 的认证 realm。提供后401 响应的头变为WWW-Authenticate: Basic realm...浏览器提示框可显示该 realm 文案descriptionNone安全方案的描述文本同样写入 OpenAPIauto_errorTrue为True时缺少合法认证头会自动抛 401为False时依赖返回None可用于“可选认证”或多种认证方式并存的场景其中realm的效果由make_authenticate_headers()生成fastapi/security/http.py#L197-L200def make_authenticate_headers(self) - dict[str, str]: if self.realm: return {WWW-Authenticate: fBasic realm{self.realm}} return {WWW-Authenticate: Basic}测试 tests/test_security_http_basic_realm.py 验证了HTTPBasic(realmsimple)未认证响应中WWW-Authenticate头确为Basic realmsimple测试 tests/test_security_http_basic_optional.py 则展示了auto_errorFalse时缺少凭据不再报错、依赖结果为None的可选认证模式。请求解析流程HTTPBasic作为依赖被调用时的核心逻辑fastapi/security/http.py#L202-L219async def __call__(self, request: Request) - HTTPBasicCredentials | None: authorization request.headers.get(Authorization) scheme, param get_authorization_scheme_param(authorization) if not authorization or scheme.lower() ! basic: if self.auto_error: raise self.make_not_authenticated_error() else: return None try: data b64decode(param).decode(ascii) except (ValueError, UnicodeDecodeError, binascii.Error) as e: raise self.make_not_authenticated_error() from e username, separator, password data.partition(:) if not separator: raise self.make_not_authenticated_error() return HTTPBasicCredentials(usernameusername, passwordpassword)可以梳理出五道防线取头与拆方案从Authorization头取值通过 fastapi/security/utils.py 的get_authorization_scheme_param()按第一个空格partition( )拆出scheme与param两部分方案校验头缺失或scheme不是basic不区分大小写时按auto_error决定抛 401 还是返回NoneBase64 解码b64decode(param).decode(ascii)解码失败非法 Base64、非 ASCII统一转成 401不会把原始异常泄漏给客户端冒号分割用data.partition(:)拆出用户名和密码必须以第一个冒号为界所以密码中可以包含冒号没有冒号则视为非法认证返回 401返回凭据模型HTTPBasicCredentials是一个 PydanticBaseModelfastapi/security/http.py#L16-L26只有username: str与password: str两个字段可直接作为依赖的类型标注使用。以上每一条防线在测试中都有对应断言非法 Base64Basic notabase64token、无冒号的载荷b64encode(bjohnsecret)均返回401与{detail: Not authenticated}见 tests/test_tutorial/test_security/test_tutorial007.py 的test_security_http_basic_invalid_credentials与test_security_http_basic_non_basic_credentials。适用前提与安全边界HTTP Basic 认证本身只做 Base64编码不做加密凭据在网络上是可逆的。生产环境应确保服务运行在 HTTPS 之下否则用户名密码会被中间链路明文捕获示例中的“硬编码用户名密码 字符串比较”仅用于教学演示。真实项目应把凭据存于安全的存储如哈希后的密码并用恒定时间比较完成校验realm、scheme_name、description仅影响提示文案与 OpenAPI 文档展示不参与任何安全校验从源码结构看它们只被写入HTTPBaseModel与make_authenticate_headers()若需要更细粒度的认证如 Bearer Token、API KeyFastAPI 在同目录提供了HTTPBearer、HTTPDigest及 API Key 系列方案可按同样的Depends()模式接入。小结三步接入security HTTPBasic()→Depends(security)→ 类型标注HTTPBasicCredentials校验凭据时先encode(utf8)再使用secrets.compare_digest()既解决非 ASCII 字符问题又消除时序攻击面认证失败统一返回401并务必带上WWW-Authenticate: Basic头以触发浏览器重新提示从源码看HTTPBasic对 Base64 解码失败、缺少冒号、非 basic 方案等情况都有明确的 401 兜底auto_errorFalse则为可选认证留出了空间。【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表