ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HarmonyOS 7 Asset Store:碰一碰一次性凭据与重放拒绝

HarmonyOS 7 Asset Store:碰一碰一次性凭据与重放拒绝 一、第二次碰触不该重复兑换TapNonceVault 的需求看起来很简单用户在门店终端碰一下把一张临时体验券从手机转到平板。第一版只把credentialId和到期时间塞进 Share Kit 的业务数据接收端看到没有过期就兑换。联调时连续碰了三次服务端只兑换了一次两个客户端却都显示成功断网恢复后旧回调又把“待确认”改成“已完成”。真正的问题不是接口有没有回调而是应用把“收到数据”“消费成功”和“对端确认”混成了一个状态。一次性凭据必须回答四件事内容是否被篡改发送端和接收端时钟差是否仍在预算内同一个 nonce 是否已经消费确认丢失时能否安全重试而不再次兑换。本轮 Demo 工程是TapNonceVault页面CredentialRelayPage任务TAP-1608。当前凭据cred_7A91在16:08:12签发16:08:22过期TTL 10 秒设备时钟偏差420 msnonce7a91c4f2载荷摘要6e42bc19。最终状态ACK_CONFIRMED成功消费 1 次重放拒绝 2 次确认耗时 186 ms待确认数为 0。二、Share Kit 只负责送达业务语义仍要自己签名精准碰一碰把设备发现和数据交接做得很轻但业务不能把通道可用等同于凭据可信。当前实现从 Asset Store 读取应用侧密钥句柄只让系统完成签名运算不把原始密钥交给 ArkTS。签名材料使用确定字段顺序任何字段缺失或顺序变化都会产生不同摘要。下面的buildEnvelope解决载荷被替换和重复生成凭据的问题。nonce在发送前生成一次确认重试时复用同一封套绝不能重新生成 nonce否则一次业务动作会变成多个可消费凭据。typeTapEnvelope{credentialId:string;nonce:string;issuedAtMs:number;expiresAtMs:number;payloadDigest:string;signature:string;};asyncfunctionbuildEnvelope(id:string,payload:Uint8Array):PromiseTapEnvelope{constissuedAtMsDate.now();constnonce7a91c4f2;constpayloadDigestawaitDigest.sha256Hex(payload);constunsigned${id}|${nonce}|${issuedAtMs}|${issuedAtMs10_000}|${payloadDigest};constsignatureawaitAssetSigner.sign(tap_credential_key,unsigned);return{credentialId:id,nonce,issuedAtMs,expiresAtMs:issuedAtMs10_000,payloadDigest,signature};}Demo 为了让图片和日志可复核固定展示 nonce 的前 8 位正式项目不应在日志里输出完整签名。Date.now()只用于业务有效期签名校验还会结合接收时刻和允许偏差。密钥句柄跟应用账号域绑定退出账号或撤销业务资格时要删除对应资产。重复调用buildEnvelope会产生新的业务凭据因此页面按钮在状态进入SENDING后立即禁用。三、时钟偏差不是简单地多给几秒接收端第一次联调时把过期窗口放宽到 30 秒表面上解决了设备时钟不一致却把重放窗口也扩大了。现在握手阶段记录双方时间差clockSkewMs本次会话测得420 ms验收时把发送端时间映射到本地时间再检查 TTL 和 1500 ms 的最大偏差预算。这段verifyWindow解决“尚未生效”和“已经过期”被同一个错误码覆盖的问题。它只返回时间判断不写消费账便于单独注入负偏差和网络延迟。typeWindowDecisionVALID|NOT_YET_VALID|EXPIRED|CLOCK_SKEW;functionverifyWindow(envelope:TapEnvelope,receivedAtMs:number,clockSkewMs:number):WindowDecision{if(Math.abs(clockSkewMs)1500)returnCLOCK_SKEW;constissuedLocalenvelope.issuedAtMsclockSkewMs;constexpiresLocalenvelope.expiresAtMsclockSkewMs;if(receivedAtMsissuedLocal-300)returnNOT_YET_VALID;if(receivedAtMsexpiresLocal)returnEXPIRED;returnVALID;}300 ms 只是容纳回调抖动不是延长凭据寿命。检测到CLOCK_SKEW时页面要求重新建立会话不继续消费。应用进入后台后尚未开始消费的封套立即失效正在等待 ACK 的记录保留但不会重新执行兑换。这个边界避免前台凭据在后台被迟到 Share Kit 回调静默处理。这里还有一个容易误判的细节网络往返时间不能直接当成时钟偏差。Demo 在会话建立时做三次轻量探测取最小往返样本估算偏移并把测量时间一起写入会话超过 30 秒或设备时间设置发生变化就不再相信旧偏移。测试中我分别注入-1700 ms、420 ms和2200 ms只有中间一组能进入消费。前后两组都停在验证页没有创建 nonce 账本也没有触发服务端兑换。为了避免 UI 把时钟错误显示成业务失败页面将CLOCK_SKEW标成“设备时间需要校准”将EXPIRED标成“凭据已过期”。两种状态的恢复动作不同前者重建会话并重新测量后者必须由发送端创建新凭据。把它们都写成“请重试”会让用户在无效封套上重复碰触。四、一次性消费需要数据库唯一约束只在内存里维护Setnonce进程重启后就失去重放证据。TapNonceVault 用 RDB 保存消费账nonce是唯一键验证签名、时间窗口和摘要后在同一事务里插入CONSUMING只有插入成功的调用才能进入兑换逻辑。第二、第三次碰触命中唯一约束分别记为REPLAY_REJECTED。下面的consumeOnce解决并发回调和进程重启重放。业务兑换失败会把记录改为FAILED_RETRYABLE但重试仍使用同一个 nonce 和幂等业务键不能删除账本后重来。asyncfunctionconsumeOnce(db:relationalStore.RdbStore,envelope:TapEnvelope):PromiseCONSUMED|REPLAY_REJECTED{awaitdb.beginTransaction();try{constinsertedawaitNonceLedger.insertIfAbsent(db,{nonce:envelope.nonce,credentialId:envelope.credentialId,state:CONSUMING,taskId:TAP-1608});if(!inserted){awaitdb.rollBack();returnREPLAY_REJECTED;}awaitVoucherService.redeem(envelope.credentialId,envelope.nonce);awaitNonceLedger.mark(db,envelope.nonce,CONSUMED);awaitdb.commit();returnCONSUMED;}catch(e){awaitdb.rollBack();throwe;}}示例把远端兑换写在事务中是为了讲清唯一入口实际产品不应让网络请求长期占用数据库事务。更稳妥的做法是先落CONSUMING提交后用 nonce 作为服务端幂等键兑换再用第二个短事务更新状态。无论哪种实现唯一约束都必须在存储层而不是依赖按钮防抖。进程在CONSUMING状态崩溃也是边界之一。下次启动不能把它当成未消费也不能直接当成成功。恢复任务会拿 nonce 查询服务端若已兑换则补写CONSUMED并重发 ACK若未兑换且仍在有效恢复窗口则继续同一个幂等请求无法判定时进入NEEDS_RECONCILE交给后台核对。这样本地状态不会因为一次崩溃随意删除。RDB 表还记录credentialId、签名摘要、首次看到时间、最终状态和最后确认时间但不保存原始业务载荷。唯一索引建立失败会阻断页面进入 READY因为没有存储级去重能力时继续开放碰触比暂时不可用更危险。数据库关闭发生在应用级协调器退出之后页面销毁只释放查询结果集和订阅。消费账保留 24 小时后清理清理依据是服务端最终态不按页面生命周期删除。清理消费账本是调试按钮只清理测试租户且要求二次确认。页面离开时会注销碰触监听但不会销毁尚未完成的 ACK 任务它由应用级AckCoordinator接管。五、确认丢失时重发的是 ACK不是凭据第一次消费完成后接收端发送ACK_CONSUMED。发送端 1.2 秒内没收到会重发确认查询携带同一credentialId nonce接收端从账本返回既有结果不再调用兑换。当前样本 ACK 在 186 ms 到达状态从WAITING_ACK进入ACK_CONFIRMEDpending 从 1 变为 0。DevEco Studio 的故障注入连续触发三次碰触。HiLog 顺序是SIGNATURE_VALID、CONSUMED nonce7a91c4f2、两条REPLAY_REJECTED最后ACK_CONFIRMED latency186ms。右侧模拟器展示同一组 TTL、时钟偏差、摘要和计数便于确认 UI 不是从另一份模拟数据读取。模拟重复碰触复用同一封套触发两次消费入口只有测试构建可见。若 ACK 最终超时发送端显示“结果待核对”而不是“兑换失败”因为本地缺少确认并不能证明远端没有消费。这个文案差异能避免用户再碰一次造成更多重放请求。六、最终状态和产品边界手机运行页把凭据生命周期压缩成一条时间线16:08:12 签发签名与窗口通过消费一次重放拒绝两次16:08:12.186 收到 ACK。当前状态ACK_CONFIRMEDnonce 消费账为CONSUMED待确认数 0。这套 Demo 不替代服务端授权。客户端签名只能证明封套来自受控应用环境最终是否具备兑换资格仍由服务端判断设备被离线太久、Asset Store 密钥撤销、账号切换或时钟偏差超过预算都应重新发起业务流程。凭据内容也要最小化碰触通道里只放业务 ID、摘要和短期证明不放完整用户资料。我还保留了四类故障计数signatureRejected、windowRejected、digestRejected和replayRejected。它们只记录数量和匿名任务号不记录签名材料。一次回归里如果重放拒绝突然归零并不代表体验变好反而可能是唯一索引或消费入口被绕过。安全指标也要有预期基线不能只关注成功率。我最终把验收条件写成三句同一 nonce 最多产生一次业务副作用确认重试不会生成新凭据任何拒绝都有签名、时间、摘要或重放四类明确原因。精准碰一碰让交互变短但安全状态机不能随之变薄。真正可靠的体验是用户只碰一下系统却能把每一步都算清楚。
返回列表