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


所有评论(0)