认证、状态码与凭据安全

本节目标

理解“你是谁”和“你能做什么”是两个问题;会按文档使用 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 的排查顺序

  1. 请求是否发到正确 host 和环境(测试/生产)?
  2. 是否按文档使用正确 header 和 scheme?
  3. token 是否过期、撤销或属于另一个项目?
  4. token 的 scope、角色、组织和资源归属是否匹配?
  5. 服务端是否返回 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 和安全可记录的错误码。

练习与自测

  1. 为一个假想 API 写 .env.example,只包含 EXAMPLE_API_KEY= 空值;解释为什么不能填真实值。
  2. 对上表每个 4xx/5xx 状态码写出“是否自动重试、是否需要人工/配置修复、需要记录什么”。
  3. 解释 access token 与 refresh token 应发往哪些服务。
  • 我能区分认证和授权。
  • 我能解释 API key、Basic、Bearer 与 OAuth 的边界。
  • 我能针对 401、403、429、503 选择不同处理动作。
  • 我能在日志和异常中避免泄漏凭据。

参考资料

获取日期:2026-07-22。下一步:分页、限流与配额。