两部设备靠近后把一张展台资料卡交给对方,这种交互看起来像一条消息传输。消息里如果还包含“查看完整图册”的链接,应用究竟该不该立即打开?把答案全部交给传输是否成功,是个危险的简化:传输层告诉我们收到了内容,不意味着内容的字段合法、版本兼容,更不代表目标网址值得信任。本文故意不实现设备触碰或 NFC 发现,而是把问题压缩到接收回执进入业务页面之后。Demo 叫 TapSafeCard,页面是 ReceiptPage.ets,构造会话 TS-1816。它允许用户手动注入一条资料卡 JSON,展示检查结果和“确认打开”按钮状态。因此这里不会编造碰一碰 Kit 的回调名称,也不会冒充真实碰传测试。## 一、回执到达和允许跳转不是同一件事设想展会里有一个图册分享卡:协议版本 2,素材编号 CAT-042,标题 展台图册,目标地址按产品规则应是 https://assets.example.org/catalog/42。这是一条允许格式的示例地址,并非可用的线上服务。攻击或配置错误场景把主机悄悄换成 assets-copy.example.org,路径仍是 /catalog/42。如果页面只是判断字符串是否包含“assets”,或者只检查路径看起来像图册,就可能误放行。这里区分四个动作:接收原始字符串、解析协议结构、限制可打开的目标、等待用户明确确认。业务状态不能跳级。初始 IDLE,解析通过后做规则检查;在域名不允许时停在 BLOCKED_DOMAIN,并禁用“确认打开”。传输成功、合法数据、同意打开是三个证据层次,任何一个都不应借另一个状态替代。另外,卡片版本不是为了美观而放的字段。当发送方升级协议新增必填内容,旧接收方贸然按老格式读取会出现空值或错误解释。此处仅接收数值版本 2。高于或低于该版本都先拒绝,提示更新或走显式兼容策略,不让页面自行猜测新字段含义。## 二、把不可信 JSON 当成外部输入先解决字段数量少却容易失控的问题。示例结构把版本、素材 ID、标题和 URL 限定为固定字段,既不要求相册权限,也不读取任何真实碰一碰数据。JSON.parse 只负责语法解析;类型断言并不会在运行时完成安全校验,所以解析后还要做 typeof、空值、长度与字符集检查。tsinterface CardEnvelope { version: number; assetId: string; title: string; targetUrl: string;}interface GateResult { ok: boolean; reason: string; card?: CardEnvelope }function parseCard(raw: string): GateResult { if (raw.length === 0 || raw.length > 2048) { return { ok: false, reason: 'PAYLOAD_SIZE' }; } try { const card: CardEnvelope = JSON.parse(raw) as CardEnvelope; if (card === null || card.version !== 2 || typeof card.assetId !== 'string' || typeof card.title !== 'string' || typeof card.targetUrl !== 'string') { return { ok: false, reason: 'SCHEMA_MISMATCH' }; } if (!/^CAT-[0-9]{3}$/.test(card.assetId) || card.title.length < 1 || card.title.length > 40 || card.targetUrl.length > 200) { return { ok: false, reason: 'FIELD_RANGE' }; } return { ok: true, reason: 'SCHEMA_OK', card }; } catch (_) { return { ok: false, reason: 'INVALID_JSON' }; }}示例有意设置 2048 字符上限,减少超大输入把本地 UI 拖慢的风险。这是业务自己选择的保守阈值,不是华为 SDK 或系统为碰一碰规定的最大长度。生产环境还应评估 UTF-8 字节长度、嵌套深度以及数字精度;如果收到的内容来自网络,应同时在服务端实施验证,而不是相信客户端已过滤。协议版本比较使用严格的数值 2,字符串 "2" 不接受。这样可以尽早发现发送方序列化不一致,而不是悄悄把类型错误带到后面的界面。标题作为纯文本展示,不用字符串拼出 HTML;如果某个 H5 组件需要展示,要按自己的上下文做输出转义,JSON 校验并不能替代 XSS 防护。## 三、域名白名单不能写成“以某前缀开头”链接校验是第二道闸。很多示例喜欢 startsWith('https://assets.example.org'),但这种判断不能限定完整 URL 结构,对相似主机、额外路径、百分号编码都不够严谨。更安全的实现要么使用经过验证的 URL 解析器并逐项检查 scheme、hostname、port、path,要么在业务范围极小的情况下采用全串精确匹配的路径正则。本文选择后者:图册只允许固定主机、固定 /catalog/ 路由以及 1~4 位数字编号。这意味着查询参数、片段、其他端口、重定向控制参数一概不在这个演示协议中。它不提供通用网址解析器功能,也不保证后续 HTTP 重定向仍停留在允许站点,实际下载器必须再检查最终跳转目标。```tsfunction checkCatalogLink(url: string): string { // 固定业务路由;不把模糊前缀判断当域名校验。 const allowed: RegExp = /^https://assets.example.org/catalog/[0-9]{1,4}$/; return allowed.test(url) ? ‘DOMAIN_OK’ : ‘DOMAIN_NOT_ALLOWED’;}function evaluateCard(raw: string): GateResult { const parsed: GateResult = parseCard(raw); if (!parsed.ok || !parsed.card) { return parsed; } const reason: string = checkCatalogLink(parsed.card.targetUrl); return reason === ‘DOMAIN_OK’ ? { ok: true, reason: ‘READY_TO_CONFIRM’, card: parsed.card } : { ok: false, reason: reason, card: parsed.card };}````checkCatalogLink拒绝示例中的https://assets-copy.example.org/catalog/42,理由是 DOMAIN_NOT_ALLOWED。如果产品后来真的要支持二级域名,不应简单改成 includes或endsWith就上线;应列出允许的完整主机清单,明确跳转协议,并检查新域名控制权。对于用户名、国际化域名、IP 地址、端口和 URL 编码,还要采用标准化解析再比较。测试时应准备多组反例:大小写混淆、协议从 HTTPS 变 HTTP、主机后接奇怪字符、空路径、路径穿越、过长字段、未知版本、重复请求。对每组保留输入、预期 reason 和页面按钮状态。这比“链接打开了没有”更能说明门禁是否按规则执行。本文不包含外部 URL 的真实访问测试,也不会自动加载示例链接。这套规则还要警惕“看着合法、最终却不合法”的二次跳转。即使资料卡的原始地址通过checkCatalogLink,目标站点返回的重定向也可能指向其他主机。本文没有实现网络请求,因此不能宣称已经覆盖这一风险。上线时应在实际发起 HTTP 请求和最终加载页面的节点重复检查目标主机,并结合证书验证和浏览器内跳转策略;必要时干脆禁止自动跟随重定向。静态白名单减少了可攻击面,不意味着接收内容可信,更不能取代发送者身份验证。![](https://i-blog.csdnimg.cn/direct/d58283138e4b498cb3f2766ad4302043.png)第二张图是**生成的 DevEco Studio 演示界面**:左侧 TapSafeCard工程树,中间强调evaluateCard与checkCatalogLink,右侧展示拦截卡片,底部 HiLog 放构造日志。图中的模拟器和日志都是沟通方案的视觉材料,不是真机、真实碰传或编译通过的凭证。## 四、等待确认期间还要防止旧回执回写即使本例只在本地点击“模拟接收”,界面也应该保留接收序号。因为后续接上异步解析、文件预取或真正的设备消息时,用户可能连续点两张卡,第一张慢返回却覆盖第二张结果。页面被销毁后,异步回调若仍修改状态,也会让调试变得混乱。我们让每次接收创建新 epoch,过期就直接丢弃。下面的片段强调状态转换,而非设备通信:注入的 raw是构造 JSON,调用evaluateCard 后只更新当前页面;confirmOpen在状态不匹配时绝不执行跳转。本轮不写Want或浏览器启动代码,以免让未验证的导航行为混进安全检查示例。```ts@State private status: string = 'IDLE';@State private title: string = '';@State private confirmEnabled: boolean = false;private epoch: number = 0;private simulateReceive(raw: string): void { const current: number = ++this.epoch; this.status = 'CHECKING'; const result: GateResult = evaluateCard(raw); if (current !== this.epoch) { return; } this.title = result.card ? result.card.title : ''; this.status = result.ok ? 'READY_TO_CONFIRM' : result.reason === 'DOMAIN_NOT_ALLOWED' ? 'BLOCKED_DOMAIN' : 'INVALID'; this.confirmEnabled = result.ok;}private confirmOpen(): void { if (!this.confirmEnabled || this.status !== 'READY_TO_CONFIRM') { return; // 演示到显式确认门禁,不执行真实导航 } this.status = 'CONFIRMED';}aboutToDisappear(): void { this.epoch++; }```同步evaluateCard 本身不会产生真正的竞态;epoch是给未来异步接收层预留的边界说明。若实际业务采用 Promise,应在await后再次比较序号,并取消预取任务或解绑监听。这里不把confirmEnabled当作安全秘密:按钮置灰属于用户体验,真正执行外部操作的函数也必须复核状态,不能只依赖 UI 组件不可点击。构造的验收记录是一张拦截卡:任务TS-1816、版本 2、素材 CAT-042、标题 展台图册,目标为 https://assets-copy.example.org/catalog/42。因此状态 BLOCKED_DOMAIN,按钮 确认打开置灰,页面保留模拟接收。示意日志为 [TapSafeCard] TS-1816 version=2 asset=CAT-042、[TapSafeCard] TS-1816 reason=DOMAIN_NOT_ALLOWED、[TapSafeCard] TS-1816 state=BLOCKED_DOMAIN。这些是按输入推导的期望内容,不是设备日志。![](https://i-blog.csdnimg.cn/direct/9b1a5201e12d4f0ab3936ce65a21aa57.png)## 五、真正接入碰一碰时,责任边界要画在这里碰一碰负责的是交互触发和能力协作,应用必须依据当下官方能力、授权和具体 Kit 指南完成实际接入。本文的 simulateReceive只是安全模型的入站适配位置,不能拿它替代设备发现、身份认证、接收方同意或系统回调。反过来,设备协议成功也不能绕过应用自己的evaluateCard,更不能让某条 URL 越过确认界面。如果要上线,至少还需要在传输层明确消息真实性、发送方身份以及防重放设计;在业务层定义协议兼容表、白名单更新治理、深链路跳转策略和操作留痕。仅凭 version和assetId` 无法证明发送者可信。更不应该把演示里的域名当成可直接配置的生产服务器。所有拦截理由可以用于调试,但不要把用户隐私、凭据或完整回执无差别输出进正式日志。这篇文章的结论是:接收、解析、域名检查、人工确认是四个独立环节。 只要任何一步没有明确通过,就停止跳转;错误要显示得让人知道是哪一道关口失败。对于新能力,先在业务入口立好边界,比“先把设备连通再补安全”省去更多返工。官方参考与边界说明:- 华为精准碰一碰技术交流入口(场景参考,未从本文核实专有 SDK 方法签名)- ArkTS 运行时标准库与 JS/TS 支持- ArkTS 官方语言介绍与静态类型约束

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐