ArkUI QRCode:碰一碰降级页短码白名单与过期遮断【鸿蒙心迹】
示例工程:HandoffSlip|页面:TransferFallbackPage|任务:HS-1708|资源:A042|协议:HS1|演示时间:2026-10-13 06:46(UTC+8)|失效时间:2026-10-13 07:01(UTC+8)。图片、二维码载荷和日志只用于展示业务约束,并非真实碰一碰链路测试。

一、别让降级方案变成另一条隐蔽的数据通道
做“精准碰一碰”场景时,我更在意的是失败之后用户还能不能完成手头的事,而不是演示动画是否足够顺滑。设备暂时不支持相关交接能力、两端没有完成发现,或者产品策略要求离线人工接力,都可能需要一个明确的替代入口。这里我选择一个很收敛的降级页:把任务短码显示成二维码,同时保留人能读懂的任务号和资源号,供另一端通过自己的授权流程继续处理。
注意,这篇并不调用精准碰一碰系统接口,也不把二维码说成碰一碰的等价协议。HandoffSlip 只是一个独立的应用层降级原型:页面叫 TransferFallbackPage,当前任务 HS-1708、资源 A042、载荷协议 HS1。二维码承载的只是一个可见、短期有效、字段明确的工作交接提示;扫码或输入后的服务端验签、身份认证、资源权限判断都不在示例范围。这些缺口不应被一个“扫码成功”的界面掩盖。
最初容易犯的错误,是直接把完整业务对象序列化成 JSON 塞进 QRCode。对象里也许带着用户名称、相册路径、内部诊断标记乃至临时授权数据。二维码本身是可见图案,周围的人也可能拍照留存;既然是替代路径,就没有理由比主链路泄露更多内容。我的设计先规定白名单,只允许四个字段:协议标识、任务号、资源号、失效时刻。协议不支持自由扩展字段,新增字段必须升级版本和双端解析规则。
演示数据固定为 HS1|HS-1708|A042|2026-10-13T07:01:00+08:00。这个串表示协议版本、任务号、资源号和绝对到期时刻。它不是 URL,也不携带令牌密钥或用户身份。有效期仅十五分钟,便于复现页面从“可展示”切换到“已过期”的逻辑。选择带时区偏移的 ISO 时间,是为了避免不同设备把同一个本地时刻理解成不同的瞬间。真实跨区域产品应进一步考虑可信时间来源,不能仅凭终端时钟证明授权有效。
二、先构造短码,再决定是否让它可见
华为官方 QRCode 是一个展示字符串的 ArkUI 组件,接口形式是 QRCode(value: string)。官方说明给出最大支持 512 个字符,超出范围会发生截取,空内容也不是有效二维码。这里不使用这个上限作为业务目标:我的应用约束为 80 个字符。多出来的余量是为了让码面简单、降低截断风险,并不是在声称 80 是系统强制的二维码容量。关键还在于:没有通过白名单校验时,根本不创建 QRCode,而不是先展示了再盖上一个“不可用”的遮罩。
第一段代码是一个纯 ArkTS 数据边界。它对输入做格式约束,明确返回“可展示的短码”或错误原因。为了保持讲解聚焦,示例使用固定的正则、Date.parse 和显式时间参数;不接触系统账号、网络授权或存储权限。
interface SlipFields {
protocol: string;
taskId: string;
assetId: string;
expiresAt: string;
}
function makeSlip(fields: SlipFields, nowMs: number): string | null {
if (fields.protocol !== 'HS1') { return null; }
if (!/^HS-[0-9]{4}$/.test(fields.taskId)) { return null; }
if (!/^A[0-9]{3}$/.test(fields.assetId)) { return null; }
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+]08:00$/.test(fields.expiresAt)) {
return null;
}
const deadline = Date.parse(fields.expiresAt);
if (!Number.isFinite(deadline) || deadline <= nowMs) { return null; }
const value = [fields.protocol, fields.taskId, fields.assetId,
fields.expiresAt].join('|');
return value.length <= 80 ? value : null;
}
这里把时区格式暂时锁定在 +08:00,是这个本地演示协议的限制,不是 HarmonyOS 对日期输入的规定。如果未来要支持跨时区扫码,最好让协议字段统一成 UTC 或明确允许的偏移集合,并严格比较解析后的毫秒数。还有一个不显眼的坑:正则只验证了字符形状,不保证“二月三十日”这样的日历日期有意义。接收方最终仍须以可信协议解析规则校验规范化日期,不能只把 Date.parse 不返回 NaN 当作生产级完整校验。
对于隐私保护,白名单还有一个现实好处:开发者不必在扫码日志中打印完整对象。真要排查问题,只需保留任务号、协议版本、过期判断结果这类低敏信息。日志也应该有独立的保留策略,不能因为展示了“示例”二字就把生产数据复制进去。此处任务 HS-1708 和资源 A042 都是虚构编号,仅用于让文章与图片互相对应。

三、QRCode 只管显示,过期状态必须由业务层决定
第二段放在页面侧。为了让教材里的截图数值可复现,默认时钟用固定的 2026-10-13T06:46:00+08:00,不要把它当作真实用户手机当前时间。@State 的 demoNowMs 是页面使用的演示时钟,点击“模拟到期”后才切换到 07:02。实际应用应从符合安全要求的时间服务或可信校验结果获取有效性结论,至少要在页面回到前台时重新检查,而不是依靠一张永不过期的静态二维码。
@Entry
@Component
struct TransferFallbackPage {
private fields: SlipFields = {
protocol: 'HS1', taskId: 'HS-1708', assetId: 'A042',
expiresAt: '2026-10-13T07:01:00+08:00'
};
@State private demoNowMs: number = Date.parse('2026-10-13T06:46:00+08:00');
private currentValue(): string | null {
return makeSlip(this.fields, this.demoNowMs);
}
build() {
Column({ space: 14 }) {
Text('HandoffSlip').fontSize(24).fontWeight(FontWeight.Bold)
Text('任务 HS-1708 · 资源 A042').fontSize(16)
if (this.currentValue() !== null) {
QRCode(this.currentValue() as string)
.width(240).height(240)
.color(Color.Black).backgroundColor(Color.White)
Text('演示有效至 07:01(UTC+8)').fontSize(14)
} else {
Text('短码已过期,请重新发起交接').fontSize(16)
}
Button('模拟到期').onClick(() => {
this.demoNowMs = Date.parse('2026-10-13T07:02:00+08:00');
})
}.padding(20).width('100%')
}
}
这里有意不在过期状态继续调用 QRCode(value)。仅用半透明遮罩盖住码面,外部仍可能从屏幕照片里恢复图形,所以业务策略如果要求停止展示,应直接切换组件分支。另一方面,移除二维码并不等于撤销了已经拍摄或保存的码;真正防重放还得由接收系统检查到期时间、任务使用状态和权限。演示没有服务端,也不承诺扫码即完成身份验证。
还要关注重绘成本。二维码的内容在有效期间保持稳定,页面只需要在影响展示结果的状态变化时更新。若未来采用真实倒计时,没必要每秒随机重建载荷,更不能把毫秒时间戳不停拼进二维码,否则用户刚打开相机,图案就改变了。有效期可以作为一个固定字段,UI 倒计时是另一份易变化的派生信息。两者分离会让用户交接过程更可预测。
四、收到码以后不能信任“码来自自己的页面”
一旦码离开本机,它就是不可信的外部输入。有人可以手写出 HS1|HS-1708|A042|... 这样的字符串,也可以把旧码再次发给别人。第三段是接收端解码前的最低限度自检:字段数量必须为四,协议版本必须认得,重新走 makeSlip 的白名单与过期门禁,且返回的规范载荷与原文一致。它只负责拒绝格式不合法、已经过期或被附加字段的内容,仍不代表真实任务鉴权。
interface ParsedSlip { taskId: string; assetId: string; expiresAt: string; }
function parseSlip(raw: string, nowMs: number): ParsedSlip | null {
const segments = raw.split('|');
if (segments.length !== 4) { return null; }
const fields: SlipFields = {
protocol: segments[0], taskId: segments[1],
assetId: segments[2], expiresAt: segments[3]
};
const normalized = makeSlip(fields, nowMs);
if (normalized === null || normalized !== raw) { return null; }
return { taskId: fields.taskId,
assetId: fields.assetId, expiresAt: fields.expiresAt };
}
验收时应该分别输入合法演示码、错误协议头、额外第五字段、资源号包含特殊字符、已经过期的码以及超长内容。输入任意一项不符合约束,接收函数就要返回 null;UI 应展示“交接信息不可用”,而不是猜一个最相近的任务。特别是 80 字符的应用上限与官方 512 字符截取行为,是两个不同层级的保护:前者来自我们的业务规则,后者是组件的能力边界,谁也不能代替谁。

五、给降级页留一条明确的退出路
我把产品检查分成“显示前”“显示中”“接收后”三个时点。显示前验证短码的字段与长度,显示中明确有效期并可以切换过期空态,接收后重新解析并交给真实的身份和权限检查。按钮名是“模拟到期”,而不是“交接成功”;任务和资源也不用看起来像真的服务器地址。这样哪怕没有扫码设备,单凭受控输入仍能评审页面有没有把边界讲清楚。
图片中的白色主题 DevEco 页面、右侧模拟器和底部 HiLog 是独立制作的布局示意图,不是 IDE 实测截图;9:16 手机画面也只是纯屏幕示意,完整状态栏并不表示某个具体设备真的运行过该工程。所有演示时间以 UTC+8 固定样例为准。对于真正的碰一碰能力、用户授权、扫码接口和跨设备通讯,这份 Demo 均没有实现,更不会据此宣称相关系统能力可用或经过应用市场审核。
我希望读者从这里带走的不是“怎样放大二维码”,而是一套更容易移植的约束:降级通路不能扩大敏感信息范围;可见字符串不等于权限凭证;过期要影响组件是否存在;接收端永远需要重新校验。只要这几个决定清楚了,未来将二维码换成口令、深链或其他离线交接介质,业务安全边界也更容易保持一致。
官方核对资料: 华为 ArkUI QRCode 组件参考。其中确认了 QRCode(value: string)、颜色、背景及 512 字符上限。HS1 协议、80 字符限制和到期规则均是本文 Demo 自定义,不属于系统 API。
更多推荐




所有评论(0)