阅读需 8 分钟

API 令牌过期:长期代理任务的恢复规则

API 令牌过期不必让长期代理任务偏离轨道。明确刷新负责人,分类错误,限制重试,并安全恢复写入。

API 令牌过期:长期代理任务的恢复规则

当令牌过期没有明确负责人时,长期运行的代理任务会以一种特别愚蠢的方式失败。代理看到 401,就重复同一个调用,消耗速率限制额度,有时还会把一次结果不确定的写入变成多次写入。这不是认证故障,而是恢复设计失败。

应将过期视为预期中的状态变化。让一个组件负责刷新,在采取行动前先对错误分类,根据操作语义限制重试,并留下记录,让操作员能够判断远程服务是否已经接受了工作。代理不应该猜测自己能否生成凭据,也不应该猜测再次发送写入是否安全。

过期是状态变化,不是异常中断

API 令牌过期说明短期凭据的授权期限已经结束,但这并不说明任务失败。长期运行的任务可能已经完成十次远程操作,然后在下一次请求中遇到过期。恢复代码必须保留这一区别。

团队常常把四种不同事件合并到一个名为 auth_failed 的分支中。这种捷径会造成错误行为,因为每种事件都需要不同的响应:

  • 过期表示令牌生命周期结束,有权负责刷新的组件可以请求新的访问令牌。
  • 撤销表示用户、管理员或服务提供商收回了授权,刷新可能会按设计失败。
  • 请求认证无效可能表示标头格式错误、凭据类型错误、签发者不匹配或受众不匹配。
  • 权限不足表示身份仍然有效,但无法执行这项操作。

OAuth 会明确区分这些情况是有原因的。RFC 6750 规定了 invalid_token bearer 令牌错误,并说明当令牌过期、被撤销、格式错误或以其他方式无效时,资源服务器应返回带有 WWW-Authenticate 挑战的 401 响应。这是有用的协议指导,但并不意味着客户端可以盲目刷新。资源服务器只是在报告它拒绝了这次请求。

任务还有两条时间线。工作时间线记录它发现、计算、创建和确认的内容。授权时间线记录允许它采取行动的凭据代次。令牌过期时,应保留工作时间线,并将授权时间线转为 renewingblocked。不要重置整个任务,然后把这种做法称为恢复。

这个区别对代理尤其重要,因为代理会执行一连串相互依赖的调用。比如,代理创建了一个变更请求,上传了一个构件,然后在附加构件之前失去访问权限。如果从第一条指令重新开始,就可能创建第二个变更请求。在每次确认远程效果后保存持久化检查点,代理就能从缺失的附加操作继续,而不是重放整个计划。

应使用明确状态,而不是 authenticated 这样的布尔值:

ready -> executing -> authorization_expired -> renewal_in_progress
renewal_in_progress -> executing
renewal_in_progress -> authorization_blocked
executing -> outcome_unknown
outcome_unknown -> reconciled -> executing

outcome_unknown 值得拥有独立状态。服务可能已经提交了写入,但连接在调用方收到响应之前断开。令牌刷新无法解决这种不确定性。任务必须先通过操作标识符、幂等令牌或服务提供商专用查询进行查询,再尝试重新写入。

必须由一个组件负责刷新

持有刷新凭据的客户端应负责刷新访问令牌,而代理应请求执行操作,不应接收可刷新的凭据。这条规则听起来很严格,直到两个代理同时遇到过期问题。

如果每个工作进程都保存一份刷新令牌副本,每个工作进程就能独立刷新。它们会相互竞争,生成充满无关凭据事件的记录;如果刷新令牌发生轮换,还可能让一个工作进程持有的令牌失效。更重要的是,你把每个能读取任务的进程都变成了长期身份持有者。

应在代理与服务提供商之间放置凭据代理或操作网关。代理保存刷新凭据或服务凭据,获取短期访问令牌,仅在执行请求时附加其中一个,然后返回响应。代理既不会收到访问令牌,也不会收到刷新令牌。

这种分工让每个参与者都有清晰职责:

  • 代理决定要执行哪项获准操作,并提供操作输入。
  • 网关检查会话是否可以发起请求,选择凭据引用,并执行调用。
  • 当服务提供商报告已分类的过期失败时,刷新负责人执行一次刷新。
  • 操作员处理授权被撤销、新的同意要求,或需要人工介入的凭据问题。

不要因为代理可以调用 OAuth 端点,就让代理负责刷新。能力和权限是两回事。能请求日历事件或部署构件的代理,并不自动需要延长某个人或某项服务身份的有效期。

RFC 6749 将刷新令牌描述为发给客户端、用于获取新访问令牌的凭据。在架构中应按字面理解“客户端”。如果代理不是注册客户端,就不应因为它发出了 API 请求而继承客户端的刷新令牌。

确实存在任务本身负责刷新的合理场景。一个范围严格限定的机器工作负载,如果拥有自己的注册客户端、独立的存储边界,并且不代表任何人执行操作,就可以这样做。即便如此,同一身份的所有并发操作也应由一个刷新协调器服务。使用互斥锁或以凭据引用为键的 single-flight 机制。第一个失败的调用负责刷新,其他调用等待结果,而不是同时冲击令牌端点。

一份简单的所有权记录可以避免模糊设计:

{
  "credential_ref": "billing-write-prod",
  "renewal_owner": "action-gateway",
  "access_token_lifetime": "provider-defined",
  "refresh_allowed": true,
  "reauthorization_owner": "on-call-operator",
  "concurrent_refresh": "single-flight"
}

记录中只包含引用,绝不包含凭据本身。同时,它还写明了刷新失效后谁必须采取行动。如果在部署前没人能回答这个问题,任务就会在夜间以糟糕的方式回答它。

401 触发刷新前需要证据

只有响应和凭据记录共同支持“过期”诊断时,才应刷新。把每个 401 都当成过期,会掩盖配置缺陷,还可能产生一长串毫无意义的刷新尝试。

先查看服务提供商记录的错误正文、标头和令牌格式要求。一些 API 返回兼容 OAuth 的 WWW-Authenticate 值,另一些返回 JSON 错误代码。有些 API 将认证失败放在使用不同状态码的网关后面。分类器应使用该服务提供商记录的信号,然后在无法确定时安全地返回终止性错误。

下面是一份可行的分类契约:

{
  "http_status": 401,
  "provider_code": "invalid_token",
  "www_authenticate": "Bearer error=\"invalid_token\"",
  "credential_ref": "reports-read",
  "token_generation": 17,
  "decision": "renew_once"
}

只有在以下条件全部满足时,响应才可以判定为 renew_once:请求使用了网关签发或选择的凭据;该凭据存在可刷新的路径;服务提供商的信号符合文档所述的过期或无效令牌条件;并且该任务尚未刷新过第 17 代凭据。

不同失败类别应返回不同结果。标头格式错误应归入 configuration_error,由开发人员检查请求构建器。受众不匹配应归入 credential_binding_error,由相关人员修复令牌请求或资源配置。授权被撤销应归入 reauthorization_required,系统停止外部操作,并准确告知正确的操作员需要为哪个身份重新同意。403 应归入 permission_denied,刷新它只是机械式的错误操作。

时钟问题会造成数量惊人的误判。根据本地时间计算过期时间的客户端可能过早拒绝仍然可用的令牌,而时钟漂移的客户端可能发送已过期的令牌。收到令牌时记录签发方提供的过期时间,保留少量安全余量,并使用可信的系统时钟。不要让每个代理都根据解码后的令牌声明自行计算过期时间。这会重复协议逻辑,并造成彼此不一致。

不要仅仅为了判断能否信任令牌而检查令牌载荷。JSON Web Token 可能包含 exp 声明,但解码其 base64url 载荷并不能验证签名、签发者、受众或撤销状态。只有在接收它的组件完成服务提供商规定的验证后,才能将它作为提示使用。不透明访问令牌没有可检查的载荷,这也是让调用方依赖响应处理而不是研究令牌内容的另一个好理由。

重试限制可以保护远程系统和证据

在确认属于过期后,应允许一次协调刷新和一次受控重放。更多重试不会改善授权,通常只会掩盖损坏的刷新路径,让审计记录更难阅读。

重放规则取决于操作能做什么。读取请求通常可以在刷新后重放。写入请求需要更强证据,因为远程服务可能已经处理了请求,只是过期响应、超时或连接中断先到达了调用方。

设计操作接口时就应对操作分类:

操作类别示例刷新后的恢复方式
读取获取记录如果请求没有外部副作用,可重放一次
幂等写入按已知版本替换文档只有在服务提供商保证该方法和条件具备幂等性时,才重放一次
带幂等令牌的写入创建发票草稿使用完全相同的令牌和载荷重放一次
非幂等写入发送消息或触发付款先协调确认,只有远程系统确认此前没有产生效果时才执行

HTTP 方法名称本身不能决定这些问题。PUT 通常具有幂等意图,但服务提供商可能为它附加电子邮件通知或异步下游操作。如果服务提供商支持幂等字段,POST 也可能是安全的。应阅读端点契约,并测试实际行为。

对于带幂等令牌的操作,应在第一次网络调用前创建令牌,并将它与规范化请求指纹一起持久化。每次重试都发送完全相同的令牌和逻辑上相同的载荷。401 后不要生成新令牌。新令牌会告诉服务提供商这是一次新操作,从而破坏幂等性的意义。

{
  "operation_id": "job-84f3/create-draft",
  "idempotency_token": "a stable random value stored before send",
  "request_fingerprint": "method, path, normalized body hash",
  "attempt": 1,
  "authorization_generation": 17
}

“规范化正文哈希”这句话很重要。如果重试构建器修改了时间戳、数组顺序或生成的标签,就可能在不知不觉中让同一个幂等令牌对应到不兼容的请求。有些服务提供商会拒绝这种不匹配,另一些处理方式并不一致。应保存第一次序列化的请求正文,或只规范化一次并重复使用。

速率限制和网络重试逻辑必须共享同一个预算。代理先进行三次网络重试,再进行一次刷新重试,然后再进行三次网络重试,就创造了七次重复操作或使系统过载的机会。应定义操作级别的尝试预算。例如,安全的读取可以允许初始调用、一次刷新路径和一次重放。类似付款的操作可能只允许初始调用,然后进入协调确认。

被抑制的重试也要全部记录。操作员需要看到系统是因为 renewal_attempted=true 而有意停止,而不是误以为代理崩溃了。当服务提供商改变错误格式,分类器开始拒绝过去允许的恢复时,这条记录也能提供清晰信号。

检查点让任务恢复时不必编造过去

离线验证恢复记录
无需保险库密钥,就能使用 sp audit verify 离线验证 Sallyport 的哈希链加密审计日志。

长期运行的代理任务应分别保存已完成的远程效果和待处理意图,因为刷新后的令牌无法告诉你失败前发生了什么。常见的错误模式只保存聊天记录或代理的最终计划,然后在中断后让代理重建状态。

应使用可由程序协调的任务日志。每次计划执行的外部调用都需要稳定的操作 ID。每个已确认结果都应包含服务提供商的资源 ID,以及可用时的版本或 ETag 和请求指纹。每个结果不确定的操作都需要记录解决它的查询规则。

一个紧凑的检查点可以这样表示:

{
  "task_id": "release-2025-04-17-42",
  "completed": [
    {"operation_id": "create-change", "remote_id": "CR-819", "version": "6"},
    {"operation_id": "upload-bundle", "remote_id": "asset-552"}
  ],
  "pending": {
    "operation_id": "attach-bundle",
    "request_fingerprint": "POST /changes/CR-819/assets body-sha256:...",
    "reconcile": "list assets for CR-819 and match asset-552"
  },
  "authorization_state": "authorization_expired"
}

任务不需要保存每个中间思考,只需要足够的事实来确定下一步安全的远程操作。不要把机密、bearer 标头和刷新响应放进这份日志。这些材料属于凭据所有者,而不是一般任务存储。

暂停期间,版本条件仍然重要。如果任务在过期前读取了文档版本 6,过了一小时后恢复时,其他参与者可能已经修改了它。条件允许时,应使用 ETag、修订号、条件标头或服务提供商专用的并发字段。如果条件失败,就告诉代理原来的计划已经不再适用。不要因为令牌刷新成功,就让任务按照“世界属于自己”的假设覆盖更新后的状态。

自主工作需要在判断能力周围设置边界。任务可以安全地恢复一个目的地和哈希都已记录的上传操作。但在原始上下文已经过时后,它不应随意重新规划部署、修改审批记录或选择其他目标。此类操作应标记为恢复后需要重新确认。

恢复响应必须告诉代理它能做什么

操作网关应返回结构化的恢复结果,而不是含糊的认证句子,以免代理自行发挥。结果应告诉代理调用是否已经执行、网关是否刷新了授权、是否允许重放,以及是否需要人工介入。

有用的结果会将执行状态与凭据状态分开:

{
  "operation_id": "attach-bundle",
  "execution_state": "not_sent",
  "authorization_state": "reauthorization_required",
  "retry_allowed": false,
  "credential_ref": "release-api",
  "operator_action": "Reauthorize the release-api connection, then resume task release-2025-04-17-42",
  "safe_resume_from": "attach-bundle"
}

not_sent 表示网关在请求交给网络客户端之前就停止了。outcome_unknown 表示它无法作出这个保证。不要把两种情况都归入 failed。第一种可以等待重新授权,第二种必须先向远程服务协调确认,再进行任何重试。

代理还需要一套有限的恢复词汇。可以提供 completedrenewed_and_replayedneeds_reconciliationreauthorization_requiredpermission_deniedconfiguration_error 等结果。每个结果都应对应一种允许的行为。例如,代理在收到 renewed_and_replayed 后可以继续;收到 needs_reconciliation 后可以执行文档规定的只读协调查询;收到 reauthorization_required 后必须停止外部写入。

不要只把服务提供商的原始响应作为信号。原始细节有助于诊断,但代理可能会误解它们,尤其是在不同服务提供商使用不一致措辞时。应将原始响应保存在受保护的诊断记录中,并向调用方返回稳定的机器可读决定。

好的错误消息会指出身份引用和被阻止的操作,但不会暴露机密。“凭据 release-api 需要重新授权,之后才能执行 attach-bundle”能告诉操作员去哪里处理。“Unauthorized”则什么也没说明。

刷新凭据需要比访问令牌更严格的控制

要求批准敏感重放
当恢复后的任务要执行敏感操作时,可将密钥设置为每次调用都需要批准。

刷新凭据通常比它替换的访问令牌存活更久,因此需要更强的保护。不要为了应对令牌过期,就把这种长期凭据分发到每个代理工作区、构建目录、环境变量或聊天记录中。

OAuth 2.0 Security Best Current Practice,也就是 RFC 9700,建议公共客户端使用刷新令牌轮换或发送方约束的刷新令牌。服务提供商的具体支持各不相同,但安全经验始终适用:被窃取的可刷新凭据,其被滥用的时间窗口远长于普通短期 bearer 令牌。

应将刷新材料保存在操作网关的加密凭据存储中。限制哪些操作定义可以选择它;对于需要更高风险控制的操作,要求明确的人工批准;并将重新授权设为独立的操作。锁定的凭据存储必须拒绝工作,而不是允许代理退回到配置文件中复制的机密。

对于受支持的 HTTP 和 SSH 操作,Sallyport 遵循这一模式:其加密保险库存储凭据,代理通过 MCP shim 请求操作,并且只收到结果。这种分工很有用,因为当恢复出错时,代理无法将刷新凭据打印到自己的上下文中。

应谨慎处理刷新响应。一些服务提供商会轮换刷新凭据,并在使用后使旧值失效。刷新负责人必须在释放等待中的调用之前,以原子方式替换存储的值。如果它写入了新的访问令牌,却在替换刷新凭据时失败,任务可能暂时还能工作,但会在下一次过期时永久失败。

绝不要记录以下字段:Authorization、访问令牌、刷新令牌、客户端机密、签名断言或完整的令牌端点正文。当系统在脱敏器运行前复制原始请求对象时,仅靠遮盖并不够。应设计日志记录器,让它接收凭据引用和令牌代次,而不是包含机密的结构。

人工批准应恢复权限,而不是制造重试风暴

凭据锁定时阻止操作
保险库锁定后会拒绝所有操作,直到你通过 Secure Enclave 和 Touch ID 解锁。

只有当人工批准对应清晰决定时,它才有帮助:允许这次代理运行、允许这次敏感调用,或恢复已撤销的授权。过期后出现一个笼统的“重试”按钮,往往会让操作员在不清楚具体操作的情况下机械盖章。

应将各种批准分开。重新授权会为凭据所有者提供新的授权或可用的刷新路径。会话授权决定这次特定的代理进程是否可以请求操作。单次调用批准决定现在是否可以执行敏感操作。这些是不同的决定,将它们合并会造成提示过多,或形成过大的长期权限。

凭据需要重新授权时,应显示受影响的凭据引用、身份或连接标签、被阻止的操作以及任务检查点。不要显示令牌本身。操作员恢复授权后,网关只能恢复检查点中记录的待处理操作,不应悄悄重放代理聊天记录中所有失败的调用。

Sallyport 的固定决策阶梯适合这一边界:保险库锁定时拒绝所有操作;默认情况下,新代理进程需要会话批准;选定的凭据可以要求每次使用都批准。这些控制不会试图从一堆规则中推断意图,因此当过期凭据打断一次原本合法的运行时,处理会更可靠。

批准疲劳通常是设计错误。如果十个并行调用同时发现同一次过期,操作员却看到十几个提示,说明刷新协调器没有合并这个事件。应呈现一个重新授权请求,让其他调用等待,然后把最终决定报告给每个任务。

不要用批准来掩盖写入结果未知的问题。此时正确的提示应询问操作员是否希望系统协调确认远程状态,而不是是否要重试。人和代理一样,都可能误操作而造成重复。

过期测试必须覆盖结果不确定的写入和并发工作进程

一个在读取前返回一次模拟 401 的令牌刷新测试,几乎不能证明什么。真正伤害团队的失败往往发生在边界上:并行工作期间、写入提交后、刷新轮换时,或授权被撤销时。

应构建测试服务提供商或可控的 HTTP 测试装置,记录收到的操作 ID,并能在预先定义的节点注入失败。断言应同时检查远程效果记录和本地审计记录。仅仅看到最终响应成功,可能掩盖重复创建。

在信任长期运行的代理前,执行以下测试:

  1. 在读取前令访问令牌过期。确认只有一个工作进程刷新,所有等待中的读取都使用替代代次。
  2. 在幂等写入前令它过期。确认网关只刷新一次,并使用原始操作 ID 和请求正文重放。
  3. 在服务提供商记录非幂等写入后丢弃响应。确认任务进入 outcome_unknown,执行查询,并且不会产生第二个效果。
  4. 在刷新前撤销授权。确认所有相关操作都以 reauthorization_required 停止,并且不会循环调用令牌端点。
  5. 返回轮换后的刷新凭据,然后中断存储。确认网关发现替换未完成,并阻止后续刷新,而不是使用过期副本。

要明确测试时间因素。向凭据组件注入时钟,以便把过期时间安排在请求构建前、标头构建后,以及请求在队列中等待期间。等待真实令牌过期会让测试变慢,还会漏掉重要的时间边界。

最后,在无法访问保险库的情况下测试审计路径。即使无法解密每条记录,也应该能够验证尝试调用和刷新决定的顺序没有改变。Sallyport 的 sp audit verify 可以在不需要保险库密钥的情况下,验证加密审计数据上的哈希链。这正是凭据访问可能仍被锁定时,事故调查所需要的验证方式。

处理过期得当的任务,在授权失败后会减少操作。它会停止,分类失败,让一个负责人刷新,协调确认不确定性,然后只恢复记录中的操作。这种克制可以避免重复副作用,并为操作员留下证据,而不是一堆重试。

常见问题

如果 API 令牌在代理任务执行过程中​​过期,会发生什么?

访问令牌可能在请求进行期间、代理等待速率限制期间,或两个本来有效的调用之间过期。应将过期视为正常的状态转换:停止需要凭据的工作,通过所有者获取一个替代令牌,然后只恢复前置条件仍然成立的工作。

是否应该允许每个 AI 代理刷新自己的 API 令牌?

通常不应该。拥有客户端注册信息和刷新凭据的组件应负责刷新访问令牌。为每个代理提供自己的刷新凭据会造成相互竞争的刷新操作,削弱撤销能力,也让事故响应变得困难得多。

HTTP 401 是否总是意味着访问令牌已过期?

不一定。401 可能表示令牌过期、授权被撤销、Authorization 标头格式错误、受众不正确或签发者不正确。在决定刷新是解决方案之前,应检查服务提供商的错误代码和响应标头。

令牌过期错误后,代理应该重试多少次?

使用有上限的重试策略。对于已分类的过期错误,允许尝试刷新一次;只重放幂等请求,或受幂等令牌保护的请求;如果重试仍然失败,就升级处理。

自主任务的刷新令牌应该存放在哪里?

刷新令牌通常比访问令牌拥有更高权限,生命周期也更长。应将它保存在权限范围狭窄的凭据代理中,采用更严格的本地控制措施进行保护,并在客户端或操作员的信任边界发生变化时撤销它。

有了幂等键,就能保证令牌恢复安全吗?

不能。幂等性可以保护某个操作,避免产生重复副作用,而令牌刷新只能恢复授权。只要任务可能创建、扣款、发送或删除内容,安全的恢复设计就需要同时具备这两点。

代理刷新令牌时,审计日志应该记录什么?

记录凭据引用、请求指纹、响应类别、刷新尝试、令牌代次和最终结果。不要把 bearer 令牌、刷新令牌或原始 Authorization 标头写入日志。

长期运行的代理无法刷新凭据时应该怎么做?

它不应继续使用已知过期的凭据发送请求。应保存任务状态,报告需要哪项授权,然后等待指定所有者或人工操作员恢复访问权限。

投入生产前,如何测试令牌过期处理?

在生产环境前,应模拟以下情况:首次调用前过期、非幂等调用前过期、远程服务接受写入后但响应到达代理前过期,以及并发工作期间过期。这些场景能暴露简单 401 测试无法发现的重复操作问题。

反向代理可以安全地为 AI 代理管理凭据吗?

代理可以注入标头,但这并不能自动解决凭据所有权、审批或审计完整性问题。更安全的模式是将机密保存在执行外部操作的组件中,再把结果返回给代理。

Sallyport

Sallyport 替你的 AI 智能体执行 API 调用和 SSH 命令。密钥留在你 Mac 上的本地密钥库里;每次运行由你批准,每个操作都落入一份密封的审计日志。

© 2026 Sallyport · 依据 Apache-2.0 开源 · Oleg Sotnikov