认证、状态码与凭据安全
本节目标
理解“你是谁”和“你能做什么”是两个问题;会按文档使用 API key、Basic Auth 和 Bearer token;能根据常见状态码采取正确动作,并避免把凭据写进代码、URL、日志或仓库。
Authentication 与 Authorization
- 认证(authentication):证明调用方是谁,例如验证 API key 或 access token。
- 授权(authorization):判断这个身份能否读取某资源或执行某动作,常由 role、scope、组织和资源归属决定。
一个 token 有效但 scope 不足,通常属于授权问题,而不是“重新登录就必然解决”。
四类常见机制
API key
API key 通常标识项目、应用或账户。具体 header 名由厂商定义,常见形式包括:
headers = {"X-API-Key": api_key} # 按此假想服务的契约把 API key 放入 header,而不写入 URL。有些服务要求 Authorization: Bearer <api-key>;即使内容叫 API key,也必须按该服务文档传递,不能自行改 header 名。API key 适合服务端到服务端调用,但通常不能表达“某位用户委托应用访问资源”的完整授权流程。
HTTP Basic Auth
Basic Auth 把用户名和密码组合后编码传输;编码不是加密,因此必须使用 HTTPS。Requests 用 auth= 构造,不要自己拼 Base64:
import requests # 导入 Requests,让库按 HTTP Basic Auth 规范构造认证 header。
response = requests.get( # 发起受保护资源的 GET 请求。
"https://example.com/protected", # 示例 HTTPS 地址;真实项目应使用文档声明的 host。
auth=("username", "password"), # 交给 Requests 生成 Basic Auth,不手工拼 Base64。
timeout=(3.05, 15), # 限制连接与读取等待,避免网络异常永久阻塞。
)Bearer token
Bearer 的含义是“持有者即可使用”。RFC 6750 定义了常见 header 形式:
headers = {"Authorization": f"Bearer {access_token}"} # 按 Bearer 规范携带 access token;绝不打印或放入 query string。泄漏 token 的人可能直接冒用,因此需要 HTTPS、最小 scope、较短有效期、安全存储和轮换。不要把 bearer token 放在 query string 中;URL 更容易进入访问日志、历史记录或监控数据。
OAuth 2.0
OAuth 2.0 是授权框架,不等同于某一种 token 格式。典型参与者包括资源所有者、客户端、授权服务器和资源服务器。客户端先按授权流程取得 access token,再用它访问资源;refresh token(若颁发)只用于向授权服务器换取新 access token,不应发给普通资源 API。
初学阶段先掌握:
- access token 有 scope、期限和目标资源限制;
- 用户委托场景和机器自身调用会使用不同授权流程;
- 不要手写 OAuth 重定向、安全校验和 token 刷新细节,优先使用服务官方 SDK 或成熟 OAuth 库;
- OAuth 安全建议会更新,实施时同时查阅服务文档与 RFC 9700 等当前 BCP。
在 Windows 中管理本地开发凭据
代码只读取环境变量:
import os # 导入 os,用于从当前进程环境读取凭据而非把它写死在源码中。
api_key = os.environ.get("EXAMPLE_API_KEY") # 只读取变量值;日志中不要回显该值。
if not api_key: # 缺失或空值都不能继续发送可能未认证的请求。
raise RuntimeError("缺少 EXAMPLE_API_KEY") # 提示只包含变量名,不泄漏任何敏感内容。当前 PowerShell 进程临时设置占位值:
$env:EXAMPLE_API_KEY = "replace-with-local-secret" # 仅为当前 PowerShell 进程设置占位密钥;真实值不要写入历史或文档。
python .\app.py # 启动会从环境读取凭据的本地脚本。关闭终端后该进程变量消失。若项目采用 .env,只提交 .env.example,将 .env 加入 .gitignore,并确认未被 Git 既有历史跟踪。生产环境应使用部署平台的 secret manager,而不是把 token 烘焙进镜像或配置文件。
发生凭据泄漏时
从文件中删除字符串并不等于风险解除。应停止继续传播,按提供方流程撤销或轮换凭据,并在获得明确授权后处理 Git 历史;不要在聊天、日志或截图中再次粘贴原值。
状态码是决策输入
| 状态码 | 常见含义 | 客户端首要动作 |
|---|---|---|
200 OK | 成功并带响应 | 校验 content type 与数据结构 |
201 Created | 已创建资源 | 读取 body 或 Location(若提供) |
202 Accepted | 已接收、可能异步处理 | 按文档轮询任务或等待回调 |
204 No Content | 成功但没有 body | 不要调用 .json() |
301/302/307/308 | 重定向 | 检查是否预期;敏感 header 不应被错误转发到其他主机 |
400 Bad Request | 请求语法或参数有误 | 修正请求,不盲目重试 |
401 Unauthorized | 缺少或不接受认证凭据 | 检查 token 是否存在、有效、发给正确服务;按流程刷新 |
403 Forbidden | 身份已知但无权访问,或策略拒绝 | 检查 scope、角色、资源归属与策略 |
404 Not Found | 资源不存在,或服务刻意隐藏其存在 | 检查 ID、路径、版本和权限 |
409 Conflict | 与当前资源状态冲突 | 读取错误码;可能是版本冲突或重复操作 |
422 Unprocessable Content | 语法可读但字段语义/校验失败 | 根据字段错误修正 payload |
429 Too Many Requests | 触发限流 | 尊重 Retry-After,降低速率/并发 |
500 Internal Server Error | 服务端意外失败 | 有条件地重试并记录 request ID |
502/503/504 | 网关、不可用或超时 | 通常是临时故障候选,但仍受重试预算与幂等性约束 |
同一个状态码在不同 API 的业务细节不同。客户端应同时读取文档化的机器错误码,例如 invalid_scope、rate_limit_exceeded,不要解析可能翻译或改写的自然语言 message。
401 与 403 的排查顺序
- 请求是否发到正确 host 和环境(测试/生产)?
- 是否按文档使用正确 header 和 scheme?
- token 是否过期、撤销或属于另一个项目?
- token 的 scope、角色、组织和资源归属是否匹配?
- 服务端是否返回
WWW-Authenticate或机器错误码?
不要把完整 token 打印出来“确认是否读到”。只记录布尔状态,例如 credential_configured=true,必要时记录本地配置来源的名称而非值。
一个安全的请求骨架
import os # 从进程环境读取部署系统注入的 access token。
from typing import Any # 标注函数返回的 JSON object 可包含多种字段值类型。
import requests # 使用 Requests 发送 HTTP 请求并处理状态码。
def fetch_profile(base_url: str) -> dict[str, Any]: # 从给定 API 基础地址读取当前主体 profile。
token = os.environ.get("EXAMPLE_ACCESS_TOKEN") # 只从环境获得 token,不把它作为函数参数或日志字段传播。
if not token: # 没有可信凭据时不能尝试匿名/错误请求。
raise RuntimeError("未配置 EXAMPLE_ACCESS_TOKEN") # 错误文本只写变量名,不泄漏 token 内容。
response = requests.get( # 发送读取 profile 的请求;真实项目还应按策略决定重试与重定向。
f"{base_url.rstrip('/')}/v1/profile", # 规范化尾部斜杠后拼接固定资源路径。
headers={ # 明确只发送 endpoint 所需的最小 header 集合。
"Accept": "application/json", # 要求服务端返回 JSON 表示。
"Authorization": f"Bearer {token}", # 按文档规定以 Bearer header 发送 token。
"User-Agent": "agent-learning-client/0.1", # 提供无敏感信息的客户端版本标识。
},
timeout=(3.05, 15), # 设置独立连接/读取超时。
)
response.raise_for_status() # 先把 HTTP 4xx/5xx 转成异常,避免错误 body 进入成功分支。
media_type = ( # 解析成功响应的主 media type。
response.headers.get("Content-Type", "") # 缺 header 时使用空字符串,便于统一拒绝。
.split(";", 1)[0] # 去掉 charset 等参数。
.strip() # 删除两端空白。
.lower() # 统一大小写后比较。
)
if media_type != "application/json" and not media_type.endswith("+json"): # 接受标准 JSON 与供应商扩展 JSON 类型。
raise ValueError("profile endpoint returned an unexpected media type") # 不让 HTML 等内容进入 JSON 解码器。
value = response.json() # 解析已通过 HTTP 与 media-type 检查的响应。
if not isinstance(value, dict): # 本端点的最低 schema 契约要求顶层为 object。
raise ValueError("profile endpoint must return a JSON object") # 数组/标量响应需要单独的契约处理。
return value # 返回已通过最低形状检查的 profile 数据。这段代码尚未处理重试、字段级 schema 与错误 body;它的重点是:凭据从环境读取、只走文档规定的 header、设置超时、在状态成功后仍检查 JSON media type 与顶层形状,并且没有日志泄漏。
常见错误
- 把 API key 放进示例、Markdown、notebook 输出或异常消息。
- 遇到 401 就无限刷新 token;刷新失败也需要终止和报警。
- 将 403 当成服务故障重试;权限不会因等待几秒自动出现。
- 为“调试方便”关闭 TLS 证书验证。
- 将生产 token 用在本地演示或测试数据中。
- 捕获 HTTPError 后只打印
str(exc),丢掉状态码、request ID 和安全可记录的错误码。
练习与自测
- 为一个假想 API 写
.env.example,只包含EXAMPLE_API_KEY=空值;解释为什么不能填真实值。 - 对上表每个 4xx/5xx 状态码写出“是否自动重试、是否需要人工/配置修复、需要记录什么”。
- 解释 access token 与 refresh token 应发往哪些服务。
- 我能区分认证和授权。
- 我能解释 API key、Basic、Bearer 与 OAuth 的边界。
- 我能针对 401、403、429、503 选择不同处理动作。
- 我能在日志和异常中避免泄漏凭据。
参考资料
- RFC 9110: HTTP Authentication and Status Codes
- RFC 6750: OAuth 2.0 Bearer Token Usage
- RFC 6749: The OAuth 2.0 Authorization Framework
- RFC 9700: Best Current Practice for OAuth 2.0 Security
- Requests Authentication
获取日期:2026-07-22。下一步:分页、限流与配额。