先看结论与判断条件

  • 网络重发、用户再次点击和攻击者重放不是同一问题:前两者靠业务幂等处理,后者还需要令牌时效、受众校验、持有者绑定与服务端授权。
  • 幂等键标识一次业务意图,规范化请求摘要标识该意图对应的具体参数;服务端必须同时保存两者,才能区分合法重试与同键改参。
  • 请求摘要必须基于双方一致的确定性字节序列。普通 JSON 文本的属性顺序、数字和字符串表示不稳定,不能直接拿原始文本互相比较。
  • 服务端去重记录要有处理中、成功、可重试失败和终态失败等明确状态,并把缓存结果与请求摘要绑定,避免并发请求各自执行。
  • JWT 声明验证、DPoP proof、Play Integrity requestHash 和业务幂等解决不同层面的问题,任何单一机制都不能替代其余门禁。
  • WorkManager 唯一任务可以减少设备侧重复调度,但卸载重装、多设备、并发客户端和直接 API 调用仍要求服务端持有最终幂等状态。

先区分超时重发、业务重复和令牌重放

移动端调用 AI 接口时,客户端可能已经把请求送达,响应却在蜂窝网络切换、进程暂停或网关超时中丢失。此时用户看到失败并不代表服务端没有执行。如果客户端简单重发,生成任务、计费动作、配额扣减或工具调用可能再次发生;这属于传输结果未知下的业务重复风险。

用户连续点击、后台任务和前台页面同时提交,或者系统恢复后重新调度,也会产生内容相近的多个请求。它们可能属于同一个业务意图,也可能是用户确实希望再次执行。系统不能只按提示词文本去重,而要在用户创建一次操作时生成稳定 operation_id,并明确哪几次传输共享同一意图。

令牌重放是另一条风险链。攻击者取得 bearer token 后,可能从不同设备或网络构造新请求;即使服务端幂等缓存挡住某个旧幂等键,攻击者仍可换键调用其他接口。因此接口需要分别判断业务请求是否已经处理,以及提交者是否仍被授权并持有与令牌绑定的密钥或设备上下文。

三类重复现象的判断与控制
现象关键判断主要控制不能只依赖
响应丢失后重发是否同一 operation_id幂等键与结果缓存客户端超时状态
用户再次点击是否新业务意图界面状态与新操作确认提示词相似度
前后台并发提交是否共享同一任务本地唯一工作和服务端去重进程内锁
被盗 token 调用主体与持有者是否有效JWT 校验、DPoP 与授权幂等缓存
同键修改参数摘要是否与首请求一致冲突拒绝返回旧结果
服务端任务重启状态是否已持久化原子状态机与任务恢复内存缓存

让 operation_id 表示一次业务意图

operation_id 应在用户确认一次生成、分析或工具调用时创建,并在网络重试、应用进程恢复和后台接力中保持不变。它不能在每次 HTTP 请求前重新生成,否则服务端看到的永远是新操作。客户端要先把操作记录和参数落入可靠存储,再排队发送;只有用户明确发起新业务动作时才创建新标识。

幂等键可以由 operation_id、已认证用户、目标接口和规范化请求摘要派生,也可以使用随机不可预测值并由服务端保存关联。无论采用哪种方式,服务端唯一约束都应包含租户或用户边界和具体操作类型,避免两个用户碰巧使用相同键时互相读取结果,也避免同一键被挪到另一个接口。

不能把 access token、DPoP proof 的 jti 或当前时间直接混入业务幂等键。访问令牌可能在合法重试前刷新,DPoP proof 按请求更新,时间也会变化;把这些传输凭据写进键会使同一业务意图变成新记录。认证材料应单独验证,验证通过后再用稳定的业务身份参与去重作用域。

幂等标识中稳定与不稳定的输入
候选字段是否适合原因处理方式
operation_id适合一次业务意图内保持不变首次确认时持久化
authenticated_subject适合隔离不同用户和租户认证后由服务端取得
endpoint_operation适合避免跨接口复用键使用服务端规范名称
canonical_request_hash适合发现同键改参双方按固定规则重算
access_token不适合刷新后合法值会变化只用于认证校验
当前时间或随机重试号不适合每次传输都会变化记录到尝试日志

先规范化请求,再计算请求摘要

同一个 JSON 对象可以有不同属性顺序、空白和转义形式,部分数字还存在多种文本表示。若客户端对自己发送的原始文本求摘要,而网关解析后重新序列化,服务端很可能得到另一个结果。RFC 8785 JSON Canonicalization Scheme 解决的是确定性表示问题,使参与方可以对同一数据模型得到稳定字节序列。

规范化范围要覆盖真正影响业务结果的字段,例如模型、输入、工具选择、温度策略、输出格式和已选择的知识库版本。网络超时、客户端诊断、token 和 proof 等传输信息不应混进业务摘要。服务端仍需对字段做类型、长度和允许值验证,不能因为摘要匹配就接受未授权模型或工具。

若客户端语言没有经过核对的 JCS 实现,可以让服务端成为摘要权威:客户端提交 operation_id、幂等键和结构化请求,服务端验证并规范化后计算 request_hash,再把该哈希固定到幂等记录。客户端可保存自己计算的值用于诊断,但冲突判断以服务端的确定性规则为准,规则版本也要随记录保存。

请求摘要的字段边界
字段类别是否进入业务摘要示例理由
业务输入进入模型、提示、工具参数决定执行结果
操作身份进入operation_id 与操作类型区分不同业务意图
授权主体进入作用域用户或租户标识隔离结果所有权
访问令牌不进入JWT 字符串刷新会改变传输凭据
DPoP proof不进入每次请求的新 proof用于持有者验证
网络诊断不进入重试次数和链路类型只描述传输尝试

服务端用原子幂等状态机决定执行还是回放结果

服务端收到请求后,先完成认证与基础授权,再以 subject、operation 和 idempotency_key 原子创建记录。记录已经存在时,必须比较 request_hash:相同哈希表示同一业务请求的重传,可以返回处理中状态或已缓存结果;不同哈希表示调用方复用了旧键却改变参数,应返回冲突而不是执行新任务或泄露旧结果。

第一次请求创建 pending 后,只有取得执行租约的工作者能把状态推进到 running。任务完成时,结果引用、计费或配额事实与终态应在可恢复事务边界内写入。若执行器在外部模型已接受请求后崩溃,系统仍需依赖下游操作标识、供应商请求号或对账流程判断结果,不能盲目再次调用并假定无副作用。

失败也要分类。输入无效、权限不足等终态错误可以缓存并在相同请求重放时返回;临时网络失败可标记 retryable,但重试仍使用原 operation_id 和幂等键。记录保留时间不能短于业务可能重试和对账的窗口;到期后再次收到旧键时,应依据业务风险决定拒绝、查归档或视为新请求。

服务端幂等记录的状态语义
状态允许动作重发响应禁止动作
pending取得执行租约返回已接收创建第二条任务
running续租或受控恢复返回处理中并发调用下游
succeeded读取不可变结果返回相同结果引用再次扣减或生成
failed_terminal返回固定错误返回同一失败自动换键重试
failed_retryable按原键重试返回可重试状态改变业务参数
conflict保留审计事件拒绝同键改参覆盖原摘要

短期 JWT 先验证签发者、受众、时效与权限

RFC 9068 的 JWT access token profile 给资源服务器提供 issuer、audience、expiry、scope 和客户端标识等可验证声明。AI 接口收到 token 后,应校验签名算法与密钥来源、签发者、自己的受众、有效期和所需 scope,再把已验证 subject 用作幂等作用域。只解码 JWT 或只看未过期都不是授权。

令牌保持较短有效期可以缩短泄露后的可用窗口,但具体时长要由会话、设备能力和业务风险决定。移动端不应把长期访问令牌写进日志、请求正文或幂等记录;刷新令牌和访问令牌要使用平台安全存储,并在账号退出、设备丢失或风险事件中具备撤销和重新认证路径。

即使 token 声明和签名都有效,资源服务器仍需检查请求中的模型、工具、知识库和数据范围是否属于该主体。幂等命中也不能跳过认证,因为缓存结果可能包含敏感输出。每次重放查询都要重新确认调用者仍有权读取该 operation 的状态和结果,只是不会再次执行产生副作用的业务动作。

JWT access token 的资源服务器门禁
门禁检查内容失败结果边界
签名算法、密钥与签发者拒绝请求不证明设备可信
audience包含当前 AI 资源服务拒绝跨服务 token不能替代 scope
expiry当前时刻仍有效要求刷新或登录时长由风险决定
scope覆盖目标操作拒绝越权功能还要检查对象权限
subject身份稳定且未禁用拒绝读取结果用于幂等隔离
client 标识符合授权策略拒绝未知客户端不是设备真实性证明

用 DPoP 限制被盗访问令牌在别处重放

bearer token 的问题是拿到字符串的人通常就能尝试使用。RFC 9449 DPoP 让客户端为请求生成 proof,并把 access token 绑定到客户端持有的密钥。资源服务器验证 proof 的签名、目标 URI、HTTP 方法、时间、唯一标识和 token 关联后,单独窃取 access token 的攻击者缺少私钥,重放难度随之增加。

DPoP proof 与业务幂等键生命周期不同。合法网络重试仍使用同一个 operation_id 和幂等键,但每次 HTTP 尝试生成新的 proof,并使用新的 proof jti。服务端保存短期 proof 重放记录;同一个 proof 再次出现应拒绝,而同一业务请求带新 proof 到达时则进入幂等状态机,返回原结果或继续原任务。

DPoP 不能证明手机没有被控制,也不能替代权限、短期 token 和请求摘要。私钥如果能被恶意进程调用,攻击者仍可能生成新 proof;代理和 URL 规范化配置错误也会造成校验偏差。实现时要采用成熟库,严格确认 htu 与 htm,并记录拒绝原因而不记录私钥或完整敏感 token。

DPoP proof 与业务幂等键的区别
属性DPoP proof业务幂等键组合方式
目标证明请求方持有密钥标识一次业务意图先认证再去重
每次重试生成新 proof保持原值新传输映射原任务
服务端保存短期 jti 重放记录任务状态与结果分别设置生命周期
参数绑定方法、URI 与 token业务摘要和主体不可互相替代
失败含义持有者验证失败重复或同键冲突返回不同错误类别
安全边界不证明设备绝对可信不阻止 token 被盗配合授权与风控

把 Play Integrity requestHash 绑定到同一业务请求

Play Integrity 标准请求允许应用把 requestHash 放入请求上下文,服务端收到完整性响应后,对自己看到的业务请求按同样规则重算并比较。这能减少完整性 verdict 被挪用到另一组参数的风险。用于计算 requestHash 的字节必须稳定,因此应复用已经定义的规范化业务请求,而不是临时拼接容易歧义的字符串。

完整性检查与幂等状态机的顺序要明确。服务端先完成 token 和 proof 验证,解析并验证业务字段,再计算 canonical request hash,核对完整性响应中的绑定值,随后才创建或查询幂等记录。若幂等记录已成功,也应按风险策略重新确认当前调用者授权,不能因为返回缓存结果就完全跳过身份和完整性上下文。

完整性 verdict 是服务端决策信号,不是绝对可信根。设备状态、账号活动、请求频率、模型权限和历史风险仍需组合判断;缺失或不满足策略时可以限制高风险工具、要求重新认证或拒绝请求。文章没有声称某种 verdict 能阻断全部重放,真实效果只能由项目配置、设备覆盖和服务端日志验证。

让 WorkManager 重试复用操作记录而不是制造新请求

Android WorkManager 的 unique work 和 ExistingWorkPolicy 可以减少设备侧同名后台任务重复排队,并定义保留、替换或追加行为。任务名应由稳定 operation_id 派生,Worker 从本地操作表读取幂等键和业务参数;网络失败返回重试时不生成新键,也不把尝试次数写进业务摘要。

唯一任务只管理当前应用安装和 WorkManager 数据库中的调度关系。用户多设备登录、应用数据清除、卸载重装、前台直接请求和服务端恢复都可能绕过本地唯一性。因此客户端去重用于降低噪声和改善体验,服务端的原子幂等记录才是防止重复业务执行的最终门禁。

取消也需要业务语义。停止本地 Worker 只表示设备不再继续轮询,并不保证已提交的服务端任务停止。客户端应调用具备 operation_id 的取消接口,由服务端根据状态决定能否取消,并返回可审计结果;重新进入页面时查询原状态,而不是默认创建另一个生成任务。

规范化公开示例请求并模拟服务端幂等判定
import hashlib
import json
import sys
from pathlib import Path

if len(sys.argv) != 3:
    raise SystemExit('usage: check_idempotency.py request.json ledger.json')
request_path = Path(sys.argv[1])
ledger_path = Path(sys.argv[2])
if not request_path.is_file() or not ledger_path.is_file():
    raise SystemExit('request or ledger file is missing')
request = json.loads(request_path.read_text(encoding='utf-8'))
ledger = json.loads(ledger_path.read_text(encoding='utf-8'))
required = {'subject', 'operation', 'operation_id', 'payload', 'idempotency_key'}
if not required <= request.keys():
    raise SystemExit('request is missing required fields')
if not isinstance(request['payload'], dict):
    raise SystemExit('payload must be an object')
if any(isinstance(value, float) for value in request['payload'].values()):
    raise SystemExit('example profile rejects floating-point payload values')
canonical_input = {
    'subject': str(request['subject']),
    'operation': str(request['operation']),
    'operation_id': str(request['operation_id']),
    'payload': request['payload'],
}
canonical = json.dumps(
    canonical_input,
    ensure_ascii=False,
    sort_keys=True,
    separators=(',', ':'),
).encode('utf-8')
request_hash = hashlib.sha256(canonical).hexdigest()
expected_key = hashlib.sha256(
    (canonical_input['subject'] + '|' + canonical_input['operation'] + '|' +
     canonical_input['operation_id']).encode('utf-8')
).hexdigest()
if request['idempotency_key'] != expected_key:
    raise SystemExit('idempotency key does not match the stable operation identity')
scope = canonical_input['subject'] + ':' + canonical_input['operation'] + ':' + expected_key
existing = ledger.get(scope)
if existing is None:
    print(json.dumps({'decision': 'accept_new', 'request_hash': request_hash}, ensure_ascii=False))
elif existing.get('request_hash') != request_hash:
    raise SystemExit('same idempotency key was reused with different parameters')
elif existing.get('status') == 'succeeded':
    print(json.dumps({'decision': 'replay_result', 'result_ref': existing.get('result_ref')}, ensure_ascii=False))
else:
    print(json.dumps({'decision': 'resume_existing', 'status': existing.get('status')}, ensure_ascii=False))

按允许路径和拒绝路径验收整条请求链

验收至少覆盖同一请求在响应丢失后重发、并发双击、进程重启、后台接力、token 刷新和多设备提交。每个场景都核对 operation_id、幂等键、request_hash、认证 subject、下游请求号、任务状态和最终结果引用,确认合法重试返回同一状态或结果,而不是再次生成、重复计费或消耗第二份配额。

拒绝路径同样重要:同键改参应冲突,过期或错误受众 token 应拒绝,重复 DPoP proof 应拒绝,requestHash 不匹配应阻止进入业务执行,另一个用户查询原 operation 应看不到结果。测试数据必须使用公开或脱敏样本,日志只保留必要摘要和错误类别,不能把完整提示、token 或 proof 当作排障便利长期保存。

准备御盾移动 AI 接口评估时,可整理操作创建点、前后台重试路径、服务端去重表结构、token 发行与刷新方式、DPoP 或等价持有者控制、完整性绑定、下游模型调用回执及取消语义。通过御盾中央平台提交这些边界后,项目人员可据此确定需要保护的客户端代码、必须留在服务端的授权判断和待补证的真实请求链。

事实依据与适用边界

以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。

本文判断事实或工程依据适用限制
DPoP 可以把访问令牌绑定到客户端持有的密钥,并让每次 HTTP 请求携带可验证 proof。RFC 9449 OAuth DPoP 定义 proof、token 绑定和资源服务器验证要求。DPoP 不证明设备可信,也不能替代短有效期、业务授权、幂等记录或风险判断。
OAuth 部署需要防范令牌重放、授权码注入和不安全授权流程等已知风险。RFC 9700 OAuth Security BCP 汇总 OAuth 安全最佳当前实践。该 BCP 不决定具体 AI 模型、工具或商业配额的授权规则。
JWT access token 可向资源服务器提供签发者、受众、时效、scope 和客户端等声明。RFC 9068 JWT access token profile 定义相应声明和验证语义。JWT 格式不保证签发策略安全、令牌足够短命或设备未被控制。
Play Integrity 标准请求可用 requestHash 把完整性响应关联到业务请求。Play Integrity standard requests 说明 requestHash 的生成与服务端核对流程。完整性信号不是绝对可信根,最终允许、限制或拒绝仍由服务端结合业务上下文决定。
参与摘要或签名的 JSON 数据需要确定性的属性排序、字符串和数字表示。RFC 8785 JSON Canonicalization Scheme 定义 JSON 的规范化表示。JCS 只解决确定性字节表示,不提供认证、业务授权、令牌绑定或重放存储。
WorkManager 可以通过唯一任务名和 ExistingWorkPolicy 管理重复持久任务。Manage WorkManager work 说明唯一工作、取消、停止和更新等管理语义。设备侧唯一工作不覆盖多设备、应用重装或直接 API 调用,不能替代服务端幂等。
服务端应把幂等键与认证主体、操作类型、请求摘要和结果状态共同保存。工程判断:只存幂等键无法区分跨用户碰撞、跨接口误用和同键改参。具体唯一索引、事务和保留周期取决于数据库、下游副作用和业务恢复窗口。
合法网络重试应复用业务幂等键,同时为新的传输尝试生成新的 DPoP proof。工程判断:业务意图与持有者证明处在不同生命周期,需要分别去重和验证。实现仍须遵循采用的 OAuth 与 DPoP 库,并通过真实代理、URL 和时钟配置测试。

工程常见问题

移动端每次重试都生成新的幂等键可以吗?

不可以用于同一业务操作。新的键会让服务端把重发当成新任务。operation_id 和幂等键应在首次确认时持久化,只有用户明确创建新业务意图时才更换。

同样的提示词是否一定应该返回缓存结果?

不一定。提示词相同可能是用户希望再次执行。幂等判断要依赖 operation_id、主体、接口和请求摘要,而不是文本相似度或内容完全相等。

JWT 没过期是否就能继续调用 AI 接口?

不能只看过期时间。资源服务器还要验证签名、签发者、受众、scope、subject 和对象权限,并结合账号状态、请求风险及持有者证明做决定。

DPoP 的 jti 能否直接作为业务幂等键?

不适合。每次 HTTP 尝试应使用新的 proof 和 jti,而同一业务重试需要保持原幂等键。两者分别解决 proof 重放和业务重复执行。

WorkManager 使用 unique work 后还需要服务端去重吗?

需要。unique work 只覆盖当前安装中的本地调度,多设备、重装、前台调用和服务端恢复仍可能产生重复请求,最终业务门禁必须在服务端。

申请御盾移动 AI 请求链评估需要准备什么?

准备 operation 创建点、前后台重试、幂等表结构、JWT 校验、持有者控制、完整性绑定、下游调用回执和取消流程,再通过御盾中央平台提交。

想用自己的 App 验证?

提交候选包、目标系统和关键业务路径,申请御盾 PoC 与兼容性评估。

继续阅读: 移动 AI 应用的运行时安全边界