业务接入端侧风控,最容易犯的错误是把“设备返回了一个字符串”当成“交易可以放行”。放行前需要确认结果来自可信能力,针对当前业务请求产生,仍在有效期内,并且没有被另一次操作复用。
星盾机密风控适合单独建立沙箱实验,无需把支付场景塞进知识图鉴。一份测试订单用于说明调用边界。API 26 SDK 中可核对到 riskControlEngine 的风险因子导入、风险结果获取及 JWS 结果定义;具体策略开通与服务端验证材料仍需要对应业务接入条件。
先从服务端发起挑战
客户端选择测试订单后,服务端生成一次性挑战,把它与订单编号、用户会话、操作类型和有效时间关联。设备收到挑战后请求风险结果,再把原始结果送回服务端。客户端不自行解释几个字段后决定放行。
SDK 对 policyName 和 nonce 有长度约束,分别为 1~255 字节、16~66 字节。字节长度与字符数量不总相等。生成挑战时应使用安全随机源和约定编码,校验最终编码后的字节长度,避免中文或特殊编码导致参数错误。
下面是业务层的数据契约,不代表系统接口签名。
interfaceRiskChallenge{challengeId:string;orderId:string;action:'confirm-test-order';nonce:string;expiresAt:number;}interfaceRiskSubmission{challengeId:string;signedResult:string;}订单内容改变以后,原挑战应失效。否则用户确认的内容与风险判断关联的内容可能不同。测试订单不连接实际资金操作,也不记录真实账户敏感数据。
JWS 解码不等于验证
JWS 可以携带签名保护的数据,但把它按分隔符拆开并解码,只能看到内容。服务端还需要按照官方信任材料和算法要求验证签名,检查挑战绑定、有效期及业务上下文。
不要信任令牌自行声明的任意验证地址或算法,也不要用“字段存在”代替真实性检查。具体证书链、密钥轮换及声明名称必须依据接入文档实现,无法脱离这些材料给出通用验签函数。
验签成功后,风险结果仍然是业务决策的输入之一。策略不可用、设备不支持或结果过期,应进入明确的人工确认、补充验证或拒绝流程;不能统一当作低风险。
因子最小化与请求生命周期
导入风险因子前,应先列明来源、用途、保存期限与必要性。SDK 提供能力不代表应用可以任意采集通讯录、位置或其他无关信息。实验只使用接入规范允许的最小测试因子。
同一个挑战只允许一个有效决策。重复点击时复用正在进行的业务任务;客户端超时后如果旧回调到达,也要检查当前 challengeId。服务端对挑战的消费采用原子操作,避免两个并发请求都通过“尚未使用”的检查。
日志记录阶段、错误码和随机请求标识即可,不输出完整签名结果、原始因子及用户身份内容。测试失败时需要能定位是能力查询、参数检查、服务调用还是服务端验证失败。
沙箱实验如何证明边界有效
正向用例使用合法挑战和已配置策略,完成设备调用、服务端验签及一次性消费。反向用例依次替换 nonce、修改测试订单、延迟到过期、再次提交同一结果以及切换到不支持能力的设备。
预期结果应写在运行前。重放用例必须被服务端拒绝;客户端按钮变灰并不能证明后端防重放有效。修改内容后签名验证失败,也不能自动证明业务挑战绑定正确,两类检查需要分别覆盖。
遇到能力不支持或策略未开通,要保存错误阶段,停止把实验结果解释为风控能力准确率。风险识别效果还需要具有合法来源和明确标签的数据集,几次手工点击无法估计误报和漏报。
沙箱闭环需要哪些材料
完整 Demo 应交付最小客户端、沙箱验证服务、挑战状态存储和脱敏测试记录。运行记录可以展示流程和错误样例,但不能暴露信任密钥或完整安全令牌。现有实现覆盖调用与信任边界,风控效果仍需要已标注的合法样本和真实策略结果。
把验证失败与业务拒绝分别返回
| 判定阶段 | 失败时的含义 | 后续路径 |
|---|---|---|
| 信任与签名 | 结果不可接受 | 不消费为有效风控结果 |
| 挑战及订单绑定 | 结果不属于本次请求 | 要求重新发起 |
| 策略结果 | 真实结果未满足业务条件 | 按既定沙箱策略处理 |
客户端判断简单,但无法独立建立服务端挑战信任;服务端验证多一段链路,却能统一一次性消费和业务决策。实施先完成挑战存储与重放用例,再配置测试策略,最后接设备结果。没有正式验证材料时,流程测试只能使用明确标注的测试信任域,不宣称已验证平台结果。
参考:华为开发者能力介绍;接口约束核对自 API 26 SDK 的 riskControlEngine 类型声明,具体策略与信任材料以业务接入文档为准。
风险因子与 JWS 结果的调用顺序
同一次检测先导入因子,再使用相同 nonce 请求已配置的策略。异步互斥避免重复点击同时发起两套流程;导入失败不进入检测,finally 解除忙碌状态。
客户端只把 JWS 当成不透明响应交给后续处理,演示界面仅显示长度。服务端需要按受信证书链与算法验证签名,再解释策略结果;不能把成功取得字符串直接当作风控通过。
exportclassRiskProbe{privatebusy:boolean=false;asyncdetect(policy:string,nonce:string):Promise<string>{InputRules.required(policy,'policy');InputRules.required(nonce,'server nonce');if(this.busy){thrownewError('detection already running');}this.busy=true;try{awaitriskControlEngine.importRiskFactors({nonce:nonce,appFactorData:[{factorName:'lab_scene',factorValue:'exhibit_preview'}]});constresponse:riskControlEngine.RiskControlDetectionResponse=awaitriskControlEngine.getRiskControlResult({policyName:policy,nonce:nonce});returnresponse.result;}finally{this.busy=false;}}}辅助校验与边界处理使用以下实现:
exportclassInputRules{staticrequired(value:string,name:string,max:number=256):string{if(typeofvalue!=='string'||value.trim().length===0||value.length>max){thrownewError(name+' is missing or too long');}returnvalue.trim();}statichex(value:string):Uint8Array{if(!/^[0-9a-fA-F]{64}$/.test(value)){thrownewError('challenge must be 32 bytes of hex');}constbytes:Uint8Array=newUint8Array(32);for(leti:number=0;i<bytes.length;i++){bytes[i]=parseInt(value.substring(i*2,i*2+2),16);}returnbytes;}}用失败短路和互斥检查核对源码
合法策略和因子配置、服务端 nonce、证书链信任及 JWS 验签。
| 检查层 | 当前处理 | 不能替代的结果 |
|---|---|---|
| 编译 | API 26 工程进行类型检查与打包 | 目标设备支持 |
| 逻辑 | 校验输入、状态或资源释放边界 | 平台服务真实返回 |
| 联调 | 按上面的输入和条件逐步执行 | 不能以按钮文案代替核心结果 |
riskControlEngine 与 JWS 结果的版本约束,以 HarmonyOS 官方文档中心、当前 SDK 声明和业务接入文档为准。
风控请求的先后顺序和忙碌恢复
导入因子失败时不调用检测,同一时刻只执行一次检测,失败后解除忙碌状态。测试关心客户端编排;真实 JWS 内容仍由可信服务端校验。宿主测试直接载入 RiskProbe,仅以替身代替 riskControlEngine;load 与 test 属于测试框架。
test('risk: serial factor import and detection; unlock after failure',async()=>{constorder=[];letreject=true;constC=load('services/RiskProbe',{'@kit.DeviceSecurityKit':{riskControlEngine:{importRiskFactors:async()=>{order.push('import');if(reject)throwError('reject');},getRiskControlResult:async()=>{order.push('detect');return{result:'opaque-jws'};}}}}).RiskProbe;constx=newC();awaitassert.rejects(x.detect('p','n'));reject=false;assert.equal(awaitx.detect('p','n'),'opaque-jws');assert.deepEqual(order,['import','import','detect']);});test('risk: duplicate concurrent request rejected before a second import',async()=>{letresolve,calls=0;constpending=newPromise(r=>resolve=r);constC=load('services/RiskProbe',{'@kit.DeviceSecurityKit':{riskControlEngine:{importRiskFactors:()=>{calls++;returnpending;},getRiskControlResult:async()=>({result:'jws'})}}}).RiskProbe;constx=newC(),first=x.detect('p','n');awaitassert.rejects(x.detect('p','n'),/already/);resolve();awaitfirst;assert.equal(calls,1);});| 操作或边界 | 应检查的结果 |
|---|---|
| 输入不满足前提 | 不调用后续平台操作 |
| 平台拒绝或抛错 | 保留错误,不生成成功状态 |
| 再次进入或重试 | 检查旧状态不会污染新任务 |