Requests 认证机制完全指南:从 Basic、netrc、Digest 认证到自定义 AuthBase 认证处理器的实现原理
【免费下载链接】requestsA simple, yet elegant, HTTP library.项目地址: https://gitcode.com/GitHub_Trending/re/requests
本文以 Requests 官方认证文档 authentication.rst 为主体,系统讲解 Requests 支持的全部认证形态:HTTP Basic 认证及其元组简写、netrc 文件认证、Digest 摘要认证、OAuth 1/OAuth 2 生态扩展,以及如何通过继承AuthBase编写自定义认证处理器。结合 src/requests/auth.py、src/requests/sessions.py 与 src/requests/models.py 的源码,你可以不仅会"传一个 auth 参数",还能理解认证头究竟在请求生命周期的哪一步被写入、netrc 凭据按什么规则查找、Digest 认证的 401 质询-重试协议如何在底层实现,以及重定向时凭据如何被安全地剥离与重建。
认证在 Requests 中的统一抽象:AuthBase
Requests 中所有认证方式都收敛到同一个抽象:认证对象必须是"可调用对象",接收一个已准备好的PreparedRequest,修改其请求头后返回。这一点由基类定义得很直白:
# src/requests/auth.py class AuthBase: """Base class that all auth implementations derive from""" def __call__(self, r: PreparedRequest) -> PreparedRequest: raise NotImplementedError("Auth hooks must be callable.")auth参数在准备阶段的处理逻辑位于 PreparedRequest.prepare_auth,它决定了三类输入分别如何被归一化:
# src/requests/models.py(prepare_auth 核心片段) def prepare_auth(self, auth, url=""): # 1. 未显式提供 auth 时,先尝试从 URL 中提取凭据(如 http://user:pass@host/) if auth is None: url_auth = get_auth_from_url(cast(str, self.url)) auth = url_auth if any(url_auth) else None if auth: # 2. 二元元组被特殊处理为 Basic Auth if isinstance(auth, tuple) and len(auth) == 2: auth_handler = HTTPBasicAuth(*auth) else: # 3. 其他情况一律视为可调用的认证处理器 auth_handler = cast("Callable[..., PreparedRequest]", auth) # 允许认证对象对请求做修改 r = auth_handler(self) # 用修改后的请求状态更新自身 self.__dict__.update(r.__dict__) # 重新计算 Content-Length self.prepare_content_length(self.body)这解释了后文所有认证形式的通用行为:认证处理器在请求"准备"阶段被调用,而不是发送阶段。处理器对PreparedRequest做的一切修改(写入Authorization头、注册 hooks)都会被合并回请求本身。
HTTP Basic 认证:最直接的认证方式
许多 Web 服务接受 HTTP Basic 认证,这是最简单的形态,Requests 开箱即用:
>>> from requests.auth import HTTPBasicAuth >>> basic = HTTPBasicAuth('user', 'pass') >>> requests.get('https://your-host.example/basic-auth/user/pass', auth=basic) <Response [200]>由于 Basic 认证足够常见,Requests 提供了一个简写——直接传(username, password)二元组:
>>> requests.get('https://your-host.example/basic-auth/user/pass', auth=('user', 'pass')) <Response [200]>传入元组与上面使用HTTPBasicAuth实例的效果完全一致,因为prepare_auth会将其转换为HTTPBasicAuth(*auth)。
源码细节:Authorization 头是如何构造的
HTTPBasicAuth.__call__的实现只有两行(见 HTTPBasicAuth):
def __call__(self, r: PreparedRequest) -> PreparedRequest: r.headers["Authorization"] = _basic_auth_str(self.username, self.password) return r真正的工作在 _basic_auth_str 中完成,有几个值得注意的实现细节:
- 编码规则:若用户名/密码是
str,会先按latin1编码为bytes,再拼接username:password做 Base64 编码,最后加上Basic前缀。这与 RFC 中 Basic 凭证的构造方式一致。 - 类型兼容警告:为了向后兼容历史上传入非字符串(如整数)的用法,源码保留了一段兼容代码:非
str/bytes的凭据会被转为字符串并触发DeprecationWarning,源码注释明确说明该行为将在 3.0.0 移除。 - 同族类 HTTPProxyAuth:HTTPProxyAuth 继承自
HTTPBasicAuth,仅把目标头从Authorization换成Proxy-Authorization,用于代理认证场景。
netrc 认证:从文件按主机名自动取凭据
当auth参数未显式给出时,Requests 会尝试从用户的 netrc 文件中查找目标 URL 主机名的凭据。这一默认行为由 Session.prepare_request 触发:
# src/requests/sessions.py(prepare_request 片段) # 若未显式设置 basic authentication,则尝试从环境(netrc)中读取 auth = request.auth if self.trust_env and not auth and not self.auth: auth = get_netrc_auth(url)按官方文档的约定:
- netrc 文件优先级高于用
headers=手工设置的原始 HTTP 认证头; - 若在某主机名下找到凭据,请求将以HTTP Basic 认证方式发出;
- Requests 按顺序在
~/.netrc、~/_netrc、以及环境变量NETRC指定的路径中查找(~在 Unix 下是$HOME,Windows 下是%USERPROFILE%)。
源码细节:get_netrc_auth 的查找与容错逻辑
查找逻辑在 get_netrc_auth 中,行为要点:
netrc_file = os.environ.get("NETRC") if netrc_file is not None: netrc_locations = (netrc_file,) else: netrc_locations = (f"~/{f}" for f in NETRC_FILES) # ".netrc", "_netrc"- 环境变量
NETRC存在时只查这一个路径,不再回退到~/.netrc; - 对 URL 做
urlparse取出hostname,用标准库netrc.netrc(path).authenticators(host)按主机名匹配; - 对"login 为空"的 netrc 条目(仅
password字段)做了兼容:login_i = 0 if _netrc[0] else 1,即自动改用第二个字段作为登录名; - netrc 文件解析失败(
NetrcParseError)或读取权限问题(OSError)时静默跳过netrc 认证,不抛出异常——除非显式传入raise_errors=True。
用 trust_env 关闭 netrc 行为
netrc 的读取受 Session 的trust_env开关控制(默认True,见 Session 初始化)。在需要隔离环境凭据的场景(如测试、多租户服务)可以显式关闭:
>>> s = requests.Session() >>> s.trust_env = False >>> s.get('https://your-host.example/basic-auth/user/pass')设置为False后,prepare_request中if self.trust_env and ...的 netrc 分支不再执行,同时重定向时的 netrc 重建(见下文)也会被跳过。
Digest 认证:两次往返的质询-应答协议
Digest 认证是另一种非常流行的 HTTP 认证形式,Requests 同样开箱即用:
>>> from requests.auth import HTTPDigestAuth >>> url = 'https://your-host.example/digest-auth/auth/user/pass' >>> requests.get(url, auth=HTTPDigestAuth('user', 'pass')) <Response [200]>表面用法和 Basic 一样简单,但底层实现复杂得多——Digest 协议要求先用一次"裸"请求换取服务器质询(nonce),再携带计算出的摘要重发。这部分逻辑全部封装在 HTTPDigestAuth 中:
认证对象注册 response hooks
HTTPDigestAuth.__call__并不直接写Authorization头,而是做了三件事(见call):
def __call__(self, r: PreparedRequest) -> PreparedRequest: self.init_per_thread_state() # 若已有缓存的 nonce,直接构造摘要头,跳过 401 质询 if self._thread_local.last_nonce: _digest_auth = self.build_digest_header(r.method, r.url) if _digest_auth: r.headers["Authorization"] = _digest_auth # 记录 body 的文件位置,便于 401 后重新发送 if (tell := getattr(r.body, "tell", None)) is not None: self._thread_local.pos = tell() # 注册两个响应 hooks:处理 401 与重定向 r.register_hook("response", self.handle_401) r.register_hook("response", self.handle_redirect) self._thread_local.num_401_calls = 1 return r这里正是官方文档所说"部分认证形式会额外注册 hooks 以提供进一步功能"的实例:认证处理器在请求准备阶段挂上responsehooks,由 hooks 系统 在拿到响应后按序分发。
401 处理:解析质询、重放请求
handle_401(见 handle_401)是协议的核心:
- 只对
4xx响应尝试认证(非 4xx 直接放行,源码注释引用了该行为的 issue 记录); - 检查
WWW-Authenticate头中是否含digest,且 401 重试次数num_401_calls < 2(即最多重试一次); - 若请求体是文件对象,先
seek回记录的位置,保证重放时 body 完整; - 用
parse_dict_header解析出 realm/nonce/qop/algorithm 等质询参数,复制到请求副本上附加 Digest 头后,复用原连接重新发送,并把第一次的 401 响应挂进history; - 若重试后仍失败,则复位
num_401_calls并返回原始响应,交由调用方处理。
摘要计算:build_digest_header
摘要头的构造在 build_digest_header,实现了对 RFC 2069/2617 主要算法的覆盖:
| 服务端声明的 algorithm | 实际哈希 | 备注 |
|---|---|---|
| 未声明 | MD5 | 协议默认值 |
MD5/MD5-SESS | MD5 | MD5-SESS额外把 nonce 与 cnonce 折叠进 HA1 |
SHA | SHA-1 | 源码中按算法名SHA匹配 |
SHA-256 | SHA-256 | |
SHA-512 | SHA-512 |
关键计算步骤:A1 = username:realm:password,A2 = METHOD:path(path 取 request-uri,为空时回退为/,query 会拼回),HA1/HA2分别取哈希后按qop计算respdig。此外实现还处理了两个容易踩坑的状态问题:
- 线程局部状态:
last_nonce、nonce_count、chal等全部存放在threading.local()中(init_per_thread_state),同一认证实例可被多线程安全复用; - nonce 计数:当服务端复用上次的 nonce 时
nonce_count自增并格式化为 8 位十六进制(nc),cnonce则由 nonce、计数、当前时间与 8 字节随机数做 SHA-1 后截断 16 位生成; - 重定向复位:
handle_redirecthook 在遇到重定向时把num_401_calls复位为 1,允许在跳转后的新 URL 上重新走一轮质询。
OAuth 认证:借助 requests-oauthlib 生态扩展
OAuth 是很多 Web API 的主流认证形式,但不在 Requests 核心实现内,官方文档指引使用社区库requests-oauthlib。
OAuth 1
>>> import requests >>> from requests_oauthlib import OAuth1 >>> url = 'https://api.twitter.com/1.1/account/verify_credentials.json' >>> auth = OAuth1('YOUR_APP_KEY', 'YOUR_APP_SECRET', ... 'USER_OAUTH_TOKEN', 'USER_OAUTH_TOKEN_SECRET') >>> requests.get(url, auth=auth) <Response [200]>OAuth 流程的细节请以 OAuth 官方规范与requests-oauthlib项目仓库为准(仓库内不维护该库的实现)。
OAuth 2 与 OpenID Connect
requests-oauthlib同样覆盖 OAuth 2,它是 OpenID Connect 的底层认证机制。该库提供四种凭据管理流程,可按应用形态选择:
- Web Application Flow:有服务端、可安全保管 client secret 的网页应用;
- Mobile Application Flow:无服务端、secret 无法保密的移动/桌面应用;
- Legacy Application Flow:旧式(password 模式)流程;
- Backend Application Flow:服务端到服务端(client_credentials)场景。
由于 Requests 的认证接口是"可调用对象"这一开放约定,requests-oauthlib的 auth 对象可以无缝传入auth=参数,与本文其他认证方式使用方式一致。
其他认证形态:Kerberos 与 NTLM
Requests 的设计允许其他认证形式被便捷地插入,开源社区为更复杂或较少使用的认证编写了处理器的典型代表是:
- Kerberos(
requests-kerberos):常用于企业内网、KDC 体系下的服务互信; - NTLM(
requests-ntlm):常见于 Windows 域环境。
这两个项目均维护在 Requests 官方组织名下,使用前按其各自仓库的说明安装配置即可,核心库中不包含它们的实现。
自定义认证:继承 AuthBase 编写新处理器
当现有实现不能满足需求时,可以自己实现一种认证形式。方法就是继承requests.auth.AuthBase并实现__call__()方法:
>>> import requests >>> class MyAuth(requests.auth.AuthBase): ... def __call__(self, r): ... # 在此实现我的认证逻辑,例如写入自定义头 ... r.headers["X-Api-Key"] = "my-secret-key" ... return r ... >>> url = 'https://your-host.example/get' >>> requests.get(url, auth=MyAuth()) <Response [200]>结合前面 prepare_auth 的源码可以给出两条编写要点:
__call__在请求准备阶段执行,因此它必须完成让认证生效所需的全部工作(修改请求头是最常见的做法)。由于prepare_auth在调用后会用返回对象的__dict__更新自身并重新计算Content-Length,处理器修改了 body 也能被正确反映到最终请求中;- 复杂协议可以注册 hooks。Digest 认证就是范例:
__call__里只负责"能省则省"(有缓存 nonce 就直接带摘要头),并把 401 重试逻辑挂在responsehook 上。如果你的认证协议也需要"看响应再决定"(如多因子挑战、令牌刷新),同样的模式可以直接套用。
更多实现范例可参考 auth.py 中的HTTPBasicAuth、HTTPProxyAuth、HTTPDigestAuth三个内置类,以及 Requests 组织名下的社区认证库。
重定向时的凭据安全:rebuild_auth
一个容易被忽略但生产上重要的细节:跟随重定向时,Authorization 头可能把凭据泄漏到另一个域。Requests 在 Session.rebuild_auth 中做了"智能剥离与重建":
# src/requests/sessions.py(rebuild_auth 核心片段) if "Authorization" in headers and self.should_strip_auth(original_url, url): # 重定向到新主机时,剥离认证头,避免凭据泄漏 del headers["Authorization"] # netrc 中可能为新主机准备了凭据,重新应用 new_auth = get_netrc_auth(url) if self.trust_env else None if new_auth is not None: prepared_request.prepare_auth(new_auth)即:跨主机跳转时删除Authorization头,同时(在trust_env为True时)为新主机重新查一次 netrc。这与 Digest 的handle_redirect复位逻辑共同保证了多跳认证场景下凭据既不会泄漏、也不会丢失。
小结
Requests 的认证体系可以归纳为一张分层地图:
- 协议内置:Basic(
HTTPBasicAuth/元组简写/URL 内嵌凭据)、Proxy(HTTPProxyAuth)、Digest(HTTPDigestAuth,含 401 质询-重试、nonce 计数、多算法支持)——全部位于 src/requests/auth.py; - 环境集成:netrc 按主机名自动取凭据,受
trust_env控制,解析逻辑在 src/requests/utils.py; - 生态扩展:OAuth 1/OAuth 2/OpenID Connect 由
requests-oauthlib提供,Kerberos/NTLM 由 Requests 组织维护的社区库提供; - 开放扩展点:
AuthBase+auth=可调用约定 + response hooks,使任何新协议都能以"写一个类"的成本接入。
理解prepare_auth(认证在准备阶段介入)与responsehooks(认证可在响应后介入)这两个时机点,就掌握了 Requests 认证机制的全部骨架,无论是排查认证失效问题还是开发自定义处理器,都可以从这两处入手。
【免费下载链接】requestsA simple, yet elegant, HTTP library.项目地址: https://gitcode.com/GitHub_Trending/re/requests
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考