首页 / 资讯中心 / 文章详情

AP2协议解析:用三层Mandate约束代理代买与支付授权

AP2协议解析:用三层Mandate约束代理代买与支付授权 ★ FEATURED ARTICLE
从去年下半年开始我陆续帮几个团队做过让代理帮用户下单这类需求几乎每次都会在同一个地方卡住代理把商品加进购物车、把支付流程跑通了但一旦用户事后说我没让它买这个整个链路就陷入扯皮。工程侧能拿出来的证据只有一堆服务端日志用户看不懂商家不认支付方更不敢认。AP2协议出现的背景正是为了解决这个尴尬——它把谁授权了什么、授权到什么程度、在什么时间点生效这些原本靠口头承诺和后台日志维持的东西变成了可被各方独立验证的数据结构。这篇内容我会沿着角色划分和交互链路两条线把AP2的核心概念拆开讲清楚包括三种 Mandate 各自承担什么职责、五类参与方分别持有什么凭证、一次完整的代理代买在网络侧到底发生了什么。适合正在做智能体购物、自动化代付、或者单纯想搞清楚代理花钱这件事如何被约束的开发者参考即使你之前完全没接触过可验证凭证也能跟着看下去。1. 代理替你按下支付按钮之后责任归谁1.1 传统支付的信任模型全都建立在人在场这个假设上过去十几年我们做的所有支付风控底层逻辑高度一致假设屏幕前坐着一个活人。短信验证码、指纹确认、3DS 跳转、支付密码、生物识别这些手段的本质都是在关键节点上向自然人索要一次确认。风控系统判断一笔交易是否可疑看的也是这个人的历史行为模式和这次是否一致。这个假设在代理场景下直接失效了。用户睡前给代理下了一句如果那个型号降到预算以内就帮我买然后去睡觉。凌晨三点价格真的降了代理自主完成下单和支付。这个时候没有任何一次向自然人索要确认的机会但交易又确实是用户授权的。你没法用短信验证码去叫醒他也不能因为没人确认就判定这笔交易非法。我见过最粗糙的做法是代理把支付密码存在本地需要时自己填。这种做法在工程上能跑通但等于把用户的全部支付能力无条件交给了代理边界完全失控。稍微好一点的做法是给代理一个额度上限但额度只能约束金额约束不了买什么——代理拿这笔额度去买一堆用户根本不需要的东西风控依然无从判断。1.2 AP2 的核心动作把我同意了从一句话变成一份可验证的数据AP2 的解法不是加强风控模型而是换了个思路——让用户的授权本身成为一份带签名的、可被独立验证的数据对象。这个对象在协议里叫 Mandate授权凭证它在整个链路中层层传递每一方都能拿到、都能验证、都能留存。打个比方传统的代理下单像口头委托朋友帮你买东西事后只能靠回忆和聊天记录对质AP2 更像去公证处开一份委托书委托书上写明买什么、预算多少、有效期到哪天代理人拿着这份委托书去办事商家看到公证书就认事后有争议直接看公证书。这个转变带来的第一个直接好处是举证责任被重新分配了。用户说我没授权买这个那就把 Intent Mandate 拿出来看如果里面明确写了品类和价格上限那授权是存在的如果代理超出了范围签名验证会失败责任在代理侧。整条链路不再需要靠谁嗓门大来定责。1.3 真正难的不是加密是把模糊的人类意图翻译成机器能校验的字段这是我在实际接入中体感最深的一点。AP2 用到的密码学手段其实并不复杂签名、哈希、选择性披露都是成熟技术。真正让人头疼的是语义映射用户说合适的时机帮我买这句话怎么变成 Intent Mandate 里的字段协议给出的答案是把意图拆成有限的、可枚举的约束条件。典型的有这么几类商品标识或品类范围、价格上限、时间窗口、商家白名单或黑名单、是否需要人工确认、单次还是可重复。这五六个维度基本能覆盖绝大多数场景剩下的模糊地带只能靠设置一个人工确认的兜底来处理。提醒一句不要指望把所有人类意图都塞进字段里。Intent Mandate 的设计初衷是约束下限也就是确保代理不会做出明显越界的事而不是完整还原用户想法。给它留一个超出范围就暂停并询问的退出通道比试图穷举所有条件要务实得多。2. 角色拆解五方各自持有什么、证明什么AP2 的角色划分看着简单但每一方在链路里承担的举证责任完全不同搞混了就会写出商家替用户签名这种逻辑上说不通的实现。2.1 用户与用户侧代理意图的发起者也是执行者这两个角色经常被当成一个但在协议里必须分开。用户是签名的主体只有真实的人或者代表人的设备才能对 Intent Mandate 做出有效签名。用户侧代理通常叫 Shopping Agent是执行者它负责比价、加购、组装 Cart Mandate、发起支付但它签名的对象是 Payment Mandate 的一部分执行声明不是用户意图本身。这个区分在实现上很关键。我见过有的团队图省事让代理用自己的一把密钥同时签用户意图和执行动作结果事后完全无法区分用户真的这么说了和代理自己编的。正确的做法是两条密钥路径用户侧的授权密钥通常是设备安全模块里的一把私钥需要生物识别解锁和代理侧的运行密钥可以是服务端托管的。前者签 Intent后者签执行声明验证方分开校验。代理这一侧还有个容易被忽略的职责它需要保存完整的交互上下文。因为争议发生时光有签名不够还得能说明我是根据什么信息做出这个判断的。比如代理判断价格降到预算以内那当时的报价快照、比价结果、时间戳都得留。这些不是协议强制的字段但实际运营中缺了会很被动。2.2 商家与商家侧代理报价的签署方商家在链路里的核心动作是对 Cart Mandate 签名。这个签名的作用是锁死——一旦签了商品明细、单价、总价、税费、运费、预计送达时间就都不能单方面改了。用户在确认付款前看到的就是这份被商家签过名的清单。商家侧代理如果有的话承担的是组装购物车的工作但签名的仍然是商家主体。这里有个实践中的细节很多商家的商品系统、促销系统、库存系统是三套独立服务组装购物车时价格可能来自不同系统容易出现页面显示 199签名时算出来 209的错位。我的做法是在签名前做一次强制对账把所有价格字段重新拉一遍并比对不一致就直接失败重试绝不允许带病签名。商家这一侧还有一个隐性责任库存与可履约性。Cart Mandate 一旦签署理论上就构成了要约用户接受后商家不能以没货了单方面撤销。所以签名前必须做库存预占这个预占的时长要跟凭证有效期对齐。2.3 凭证签发方与支付网络最后的验签关卡剩下的两个角色是Credentials Provider凭证签发方和支付网络 / 支付服务商。前者负责签发和验证可验证凭证比如这个用户确实通过了实名认证这张卡确实是这个用户持有的它提供的是身份与资质层面的背书。后者是资金真正流转的地方承担最终的验签和清算。支付网络这一侧的验签逻辑是整条链路的重心。它不能只看 Payment Mandate 本身必须沿着链条往回追Payment Mandate 引用的 Cart Mandate 哈希对不对得上、Cart Mandate 引用的 Intent Mandate 是否有效、Intent Mandate 的签名人是否就是这个付款账户的持有人。任何一环断了交易就得拒。这套链式校验是 AP2 区别于普通支付接口最明显的地方。下面这张表把五方的职责和签名对象放在一起对照接入时对着它检查基本不会漏角色持有凭证签名对象关键校验点用户身份凭证、支付凭证引用Intent Mandate意图边界是否被遵守用户侧代理运行密钥、会话上下文Payment Mandate 执行声明是否在意图范围内行动商家商户凭证、报价数据Cart Mandate价格明细是否一致、库存是否预占凭证签发方签发密钥、信任根各类凭证的签发声明凭证是否在有效期内、是否被吊销支付网络清算通道、风控规则交易回执链式校验是否全部通过3. 三种 Mandate 串起来的授权链三种 Mandate 是 AP2 的骨架理解它们各自解决什么问题比记字段名重要得多。3.1 Intent Mandate把我想要什么固化成一条可校验的边界线Intent Mandate 由用户签名是整个链条的起点。它回答的问题是在什么范围内代理可以替我做决定。字段设计上通常包含意图描述、约束条件集合、有效期、是否允许自主完成支付、以及一个指向用户身份凭证的引用。这里最容易踩的坑是把 Intent Mandate 当成订单。它不是订单它是授权边界。同一个 Intent Mandate 理论上可以支撑多次购物车组装比如代理在三个商家之间比价各生成一份 Cart Mandate只要都在边界内。我见过有实现把 Intent 写成买 A 商品 1 件价格 199那代理就彻底失去比价空间了等于退化成传统下单白白浪费了协议的价值。有效期这个字段要格外小心。太短代理还没比完价就过期了太长用户可能忘了自己授权过什么。我的经验值是即时性需求给 30 分钟到 2 小时跨天比价给 24 小时周期性补货类给 7 天但强制要求每次支付前通知。超过 7 天的授权用户体验上已经开始失焦了。3.2 Cart Mandate让这份清单变成不可抵赖的事实Cart Mandate 由商家签名内容是一份完整的、结构化的购物清单商品标识、数量、单价、折扣、税费、运费、总价、预计履约时间以及一个指向对应 Intent Mandate 的引用通常是哈希。它的价值在于消除了价格争议的空间。传统电商里用户下单后商家改价、改运费、加收服务费的情况并不少见靠的是最终以结算页为准这种模糊表述。Cart Mandate 签署之后这些字段就被钉死了任何一方想改都得重新签一份新的而新的一份会带着新的哈希跟旧的对不上验签自然失败。实现上有个细节值得说Cart Mandate 里不建议放完整商品描述。图片、详情文案这些东西体积大、变动频繁放进去会让凭证膨胀得没法传。正确做法是只放商品 ID、SKU、关键属性和一个内容哈希详情由商家侧按需提供。这样既保证了可校验性又控制了凭证体积。3.3 Payment Mandate把支付动作和前面两份凭证焊在一起Payment Mandate 是链路里最后一份由用户侧代理签名部分场景下需要用户追加确认签名。它包含支付方式引用、金额、币种、收款方、交易标识以及对 Cart Mandate 和 Intent Mandate 的引用。这份凭证的作用是让支付网络能够一次性完成链式校验。它不像传统支付接口那样只传一个金额和订单号而是带着完整的前序凭证引用网络侧可以自己往回追。这就把这笔钱该不该付的判断权从商家手里部分转移到了网络侧大大降低了商户侧被攻破或者内部作恶的风险。三种 Mandate 的对比放在一起看会更清楚维度Intent MandateCart MandatePayment Mandate签名方用户商家用户侧代理必要时用户追加核心作用划定授权边界锁定交易明细绑定支付动作与前序凭证有效期较长分钟到天较短通常 5-15 分钟一次性随交易失效可复用性可支撑多次比价一次交易一份严格一次性失效后果代理停止行动需重新组装购物车交易失败需重新发起3.4 为什么必须是三层而不是合成一份这个问题我被问过好几次。合成一份凭证字段更少、传输更简单为什么不这么做原因在于签名主体的分离。意图是用户签的清单是商家签的支付是代理签的三方在物理上就是分离的不可能由一方代签全部。如果强行合成一份要么某一方需要持有别人的私钥安全性灾难要么签名变成形式主义等于没有。另一层原因是生命周期的差异。意图可能存活几小时购物车只存活几分钟支付是一次性的。合成一份就只能取最短的那个有效期意图的价值就被浪费了。分层之后每层各自管理自己的生命周期效率高得多。4. 走一遍完整链路一次代理代买的全过程光看概念容易飘我拿一个虚构的场景把整条链路走一遍。设定是某开发者做了一个比价代理用户让它盯着某款耳机价格降到 800 元以内就买下来。4.1 第一步意图生成与用户签名用户说出需求后代理把它翻译成结构化的 Intent Mandate。这个翻译过程建议先回显给用户确认一次尤其是第一次使用时。我做的实现里确认页会把价格上限 800 元仅限官方旗舰店有效期 24 小时支付前通知我这几条用大白话列出来用户点确认之后才触发签名。签名动作必须在设备侧完成私钥不出安全模块。如果用户设备不支持退而求其次用一次性口令加服务端密钥托管但要明确告知用户风险等级不同。生成的 Intent Mandate 大概长这样字段做了简化实际规范里还有版本、签发时间、吊销信息等{ type: IntentMandate, version: 1.0, issuer: did:example:user-8f3a, issued_at: 2025-03-11T22:14:03Z, expires_at: 2025-03-12T22:14:03Z, constraints: { category: audio.headphone.wireless, max_total_price: { amount: 800.00, currency: CNY }, merchant_allowlist: [official-store-x], require_user_confirmation_before_payment: true, max_quantity: 1 }, payment_method_ref: vc:card-token-7c21, proof: { type: Ed25519Signature2020, value: z3F...kQ } }注意require_user_confirmation_before_payment这个字段。它是半自动模式的核心开关设为 true 时代理可以自己比价、自己加购但到付款前必须停下来等用户一句确认。绝大多数消费场景我都建议先设成 true跑一段时间观察代理的判断质量稳定了再考虑放开。4.2 第二步商家侧锁价与购物车凭证返回代理拿着 Intent Mandate 去商家侧发起比价请求。商家或者商家的代理验证意图凭证有效后返回一份签好名的 Cart Mandate。这一步的关键是幂等与锁价商家需要为这次报价生成一个唯一标识并在凭证有效期内锁定价格和库存。代理收到 Cart Mandate 后要做的第一件事是校验哈希链——确认这份购物车确实关联的是自己发出的那个意图而不是别处拼凑来的。校验通过后再做语义校验总价是否在预算内、商家是否在白名单里、数量是否超标。这两层校验都过了才能进入支付环节。这里有个我踩过的坑早期实现只做了签名校验没做语义校验结果代理在一个满 999 减 200的凑单活动里把两件商品凑成了 999单件折算下来超预算了但总价校验因为折扣逻辑写错而通过。后来我在校验层加了一条硬规则任何一项约束不满足整笔交易直接拒绝不做差不多就行的模糊判断。4.3 第三步网络侧验签、扣款与回执代理组装 Payment Mandate带上对前两份凭证的引用提交给支付网络。网络侧的校验顺序通常是先验 Payment Mandate 的签名再根据引用拉取 Cart Mandate 和 Intent Mandate逐层验签、逐层检查有效期、逐层比对引用关系最后核对付款账户与 Intent Mandate 签发人是否一致。全部通过之后才进入真正的资金处理。扣款成功后返回一份交易回执回执里同样带有对 Payment Mandate 的引用形成完整闭环。这份回执要原样存下来它是后续对账和争议处理的唯一凭据。整个链路里最慢的一环通常是跨方拉取凭证。如果凭证存在各自的服务器上网络侧校验时要做多次远程调用延迟会很难看。实践中常见的优化是把整条凭证链在提交支付时一次性打包带上网络侧本地校验只在需要吊销检查时才回源。这个取舍要看你更看重时延还是实时吊销能力。4.4 第四步结果回传与用户通知交易完成后代理侧要做的收尾工作包括更新本地状态、向用户推送结果、把整条凭证链归档。归档这件事看着琐碎但它是后面所有争议处理的基础。我的做法是按交易 ID 建目录把 Intent、Cart、Payment、回执四份数据连同时间戳一起存保留期跟财务对账周期对齐。用户通知这块要说一句不要只通知买成功了。要告知实际成交价、和预算的差额、商家是谁、预计什么时候到。用户对代理的信任是靠一次次透明通知累积起来的含糊其辞的通知会让人产生它是不是在偷偷花钱的疑虑。5. 工程落地时最容易翻车的几个点协议读起来很顺真写代码的时候坑都在细节里。下面这三个是我在实际项目里反复遇到的。5.1 时效窗口设计不当导致价格漂移或凭证过期三种凭证的有效期如果设成一样的几乎必然出问题。购物车凭证通常只有 5-15 分钟的有效期因为要给库存和价格留余地意图凭证可能是几小时。如果代理花 20 分钟比价等它拿到最优的购物车准备付款时购物车凭证已经过期了只能重新组装而重新组装的价格可能又变了陷入死循环。我的处理办法是把比价阶段和锁价阶段分开。比价阶段用轻量的询价接口不生成正式凭证确定要买的那一刻才正式请求 Cart Mandate拿到之后立刻发起支付中间不插入任何耗时操作。整个锁价到支付的窗口我控制在 30 秒以内实测下来失败率最低。还有一个隐性问题时钟偏移。跨方校验有效期时各方服务器时间不一致会导致我这边还没过期你那边已经过期。生产环境必须配 NTP并且在凭证里预留 1-2 分钟的容差。这个容差不能太大否则等于变相延长了有效期。5.2 幂等没做对重复扣款找上门代理场景比人手动下单更容易触发重试因为超时判断是程序做的网络抖动一下就可能重发。如果没有幂等键同一笔交易被处理两次的概率相当高。幂等键的设计要点是稳定且唯一用交易本身的核心要素哈希生成比如 Intent 标识、购物车哈希、金额、时间窗口的组合而不是用随机 UUID。随机 UUID 每次重试都会变等于没有幂等。请求头里带上这个键接收方在事务开始前先查一下有没有处理过处理过就直接返回上次的结果。这里要特别提醒幂等的作用域要明确。是商家侧幂等、支付网络侧幂等还是两边都要答案是两边都要各自维护自己的幂等表。跨系统只靠一层幂等中间环节重试照样会出问题。5.3 争议发生时的举证链比想象中更容易断真正出问题的时候你会发现有签名和能举证是两回事。签名只能证明某个数据没被篡改但证明不了当时的上下文是什么。我遇到过一次典型情况用户投诉代理买错了型号。凭证链是完整的签名也都对但问题是 Intent Mandate 里的品类约束写得太宽只写了无线耳机代理选了一个带降噪的高价型号用户认为自己说的是基础款。这种情况下凭证本身无法判定谁对只能看有没有留存当时的对话记录。所以举证链至少要包含三层凭证本身、生成凭证的输入、以及凭证被消费时的上下文。第三层最容易被忽略比如代理在决定购买那一刻看到的商品页面快照、库存状态、同款比价结果。这些东西不属于协议范畴但对争议处理至关重要。下面这张表是我整理的常见故障对照接入时可以直接拿去当检查清单用现象根因处理方式支付时提示凭证已过期锁价到支付间隔过长或时钟偏移压缩窗口到 30 秒内配置 NTP 与容差用户否认授权Intent 边界描述过宽收窄约束字段增加人工确认节点重复扣款幂等键使用随机值改用交易要素哈希双端各自维护幂等表商家改价后交易仍通过只验签名未做语义校验增加价格、数量、商家白名单的强制校验吊销状态未及时生效全链路本地校验未回源查询关键场景增加实时吊销检查接受额外延迟凭证体积过大传输失败把商品详情塞进了凭证只放 ID 与哈希详情按需拉取5.4 一个不太起眼但很要命的点代理的退出机制代理执行到一半发现条件不满足比如价格一直没降到位、或者商家下架了商品它该怎么结束很多实现里根本没考虑这个代理就一直挂着Intent Mandate 到期自动失效用户完全不知道发生了什么。正确的做法是定义明确的终止状态并通知用户。至少要有这么几种条件满足已完成、授权过期未执行、条件不满足主动放弃、发生错误需人工介入。每种状态都要给用户一个可读的说明而不是静默消失。这个细节对用户信任的影响比很多技术优化都大。6. 自己动手接入时的取舍与验证清单6.1 先跑通人在场模式再考虑全自动AP2 支持两种执行模式用户在场确认user present和完全自主autonomous。我的建议毫无悬念——先把人在场的链路跑通跑稳再去碰自主模式。人在场模式的链路和自主模式在凭证结构上是一样的区别只在于支付前多一次用户确认。先把这条链路走通你能验证签名、验签、哈希链、有效期、幂等这些基础能力是否扎实。等这些稳了再把确认环节去掉风险可控得多。反过来先做全自动一旦出问题你连是哪一环错的都定位不了。实际操作上我一般会设一个灰度开关按用户维度控制是否启用自主模式先给内部测试账号开再给少量愿意尝鲜的用户开观察一周的异常率再扩大。异常率的核心指标是用户主动申诉的比例和代理放弃执行的比例前者高说明边界太宽后者高说明条件太严。6.2 沙箱验证要覆盖的异常路径正常路径跑通不代表能上线。我整理了一份异常路径清单每次接入新版本都会过一遍凭证过期后再提交支付是否正确拒绝并返回明确错误码购物车哈希被篡改一个字节验签是否失败引用指向一个不存在的意图凭证是否拒绝而非静默通过同一份 Payment Mandate 提交两次第二次是否被幂等拦截商家在签署购物车后修改价格再次校验是否失败凭证链中某一环被吊销整条链路是否立即失效代理在支付前进程崩溃重启后是否能恢复或正确终止这份清单看着基础但每一条我都见过真实系统栽在上面。尤其是引用指向不存在凭证这一条很多实现为了容错会写成查不到就跳过校验等于把整个链式校验废掉了。6.3 关于凭证存储位置的选择凭证存在哪是个需要提前想清楚的问题。三个选项各有代价存在用户设备上最安全但代理跑在服务端时取用不便存在代理服务端方便但等于把授权凭证交给了执行方保管内控要求高存在独立的凭证服务上最规范但多了一次网络调用时延和可用性都要额外考虑。我的实践是分层存Intent Mandate 的私钥永远在用户设备服务端只存签名后的凭证Cart Mandate 和 Payment Mandate 存服务端因为它们本来就是短生命周期的交易回执双份存服务端一份、用户可下载一份。这个组合在安全性和可用性之间比较平衡。6.4 最后分享一个排查技巧调试凭证链问题的时候人眼去看一长串 Base64 基本没戏。我的做法是写一个小工具把凭证解开之后按签发方—签名对象—引用关系—有效期四个维度平铺成一张表任何一个环节对不上会高亮。这个工具花了我半天时间但后面每次联调至少省下几个小时。如果你准备长期做这块建议第一天就把这个工具建起来。另外提醒一点日志里不要打完整凭证。凭证里通常含有用户标识、支付方式引用这类敏感信息直接落到日志系统里是合规隐患。我的处理是只打凭证 ID、签发方、有效期和校验结果需要看内容时按 ID 去凭证库里取并且取用行为本身要留审计记录。
阅读完成 · 觉得有帮助?
咨询建站