分享一张活动卡片,看起来只需要把标题、地址、时间和链接放进一个对象。实际工程最容易漏掉的,是这张卡片可能由报名表、运营后台、联系人管理和站点配置共同拼接而来。一份含12个字段的源对象里,面向接收方真正需要的也许只有5个;如果把整个对象序列化再交给共享通路,用户表单中的电话号码、内部备注和临时口令就会跟着出去。
这篇演示工程叫CardBoundary,选的是线下活动“城市开发者夜校”。源卡片event_talk_42有12个字段,产品只允许发布标题、场地简称、活动时间、类别和公开URL这5个字段,剩余7个明确丢弃。我们使用UDMF的纯文本记录和超链接记录这两个标准数据类型装配分享内容,问题焦点不在“能否碰到对方”,而是“发送前的数据已经缩到允许的边界”。真正的碰一碰设备发现和系统投递都没有在本稿执行,页面状态只能叫EXPORT_READY,不能叫“已发送成功”。
一、一份12字段源数据,最危险的是字段外溢
先固定不可漂移的数据契约。示例任务CARD-1010-12,活动IDevent_talk_42,活动标题“城市开发者夜校”,场地简称“创智中心A厅”,开始时间为“10月12日 19:30”,分类“技术沙龙”,公开地址是https://events.example.org/agenda/42。域名属于示例用途,不代表真正控制了外部站点。
源对象一共12个字段。白名单包括title、place、startAt、category、publicUrl,也就是5个;明确不允许共享的是organizerPhone、mobile、email、internalNote、privateToken、attendeeList、addressExact,共7个。第二个列表只记录字段名,不把实际敏感字段值再写入日志或截图。这样做比展示一串被部分打码的电话更可靠,也便于用自动化断言最终导出对象里完全不存在这些键。
源输入还包括一个单独的恶意链接夹具javascript:alert(1)。它应该被应用层URL策略拒绝一次,诊断数为unsafeLinkBlocked=1;它不属于本次正式活动对象的5个发布字段,也不会导致主活动卡片本身失效。把两个用例并列展示,是为了说明“成功导出的合法对象”和“被拦住的异常测试向量”不是同一个数据包。最终状态为EXPORT_READY,统计sourceFields=12、allowed=5、redacted=7、representations=2。
这个题目常被误写成“字段脱敏只要把显示文字打星号”。如果源对象完整保留并写入后台日志、URL查询参数或UDMF附加字典,用户虽然看不到字段,原始信息仍然可能被其他消费者读到。本例选择按允许列表构建一个全新的对象,从生成源头断开7个不允许键的引用,而不是在原对象上不断delete或把敏感值替换成星号。
二、先厘清UDMF做什么、应用自己做什么
华为UDMF统一数据通路文档介绍了UnifiedData、UnifiedRecord以及PlainText、Hyperlink等数据类型。UniformDataType.PLAIN_TEXT对应通用纯文本,UniformDataType.HYPERLINK对应超链接;统一数据对象可以通过addRecord包含多条记录,并通过getRecords查看内容。官方的统一数据类型枚举也列出了HTML,但这不表示把任意外部HTML直接加入分享包就是安全做法。本例刻意不输出HTML,以免把不需要的复杂渲染内容交给接收方。
UDMF解决的是不同应用之间对数据类型的表达和交换问题,不负责判断“报名手机号是否应该被分享”。另外,往UnifiedData里加两条记录,不等于系统已经发现另一台设备或发生了碰一碰传输。接收方能否收到、设备是否支持、所处场景的权限和生命周期约束都必须走官方对应的投递流程验证。本文先把数据产品层的门禁做完,再把输出对象交给受支持的发送入口,二者职责不能互换。
决定使用纯文本+超链接两条记录,是因为目标接收方可能没有对应活动卡片的专用UI。最差情况下它至少能看到纯文本的标题、场地和时间;支持链接类型的接收方可以选择打开公开URL。这里也存在产品取舍:如果为了展示丰富卡片而塞入源表单的完整JSON,接收方就需要理解自定义协议版本,还可能被迫接收无关字段,安全面和兼容性都会扩大。
我们把两条记录看作同一个清理后的业务快照的两种表示,而不是两个分别拼接的消息来源。只要两种表示在时间或活动ID上不一致,就拒绝导出。UDMF允许多条记录,但跨记录一致性校验由应用负责;不能推断系统会帮忙确认超链接描述和纯文本介绍引用的是同一场活动。
三、应用层白名单:减少数据才是默认值
第一段代码解决“源对象不能原样进入分享层”的问题。为避免平台接口混入业务算法,纯数据模型单独放在model/CardWhitelist.ets,用明确字符串键提取允许字段,并把任何缺失必填键作为硬错误。这里示例数据从本地夹具而来,不使用账号数据。生产实现应对每个字段再做长度限制、控制字符处理和Unicode归一化,且不能把异常时的完整源对象打印到日志。
// model/CardWhitelist.ets —— 纯ArkTS业务模型,无虚构系统APIexportinterfacePublicCard{title:string;place:string;startAt:string;category:string;publicUrl:string;}constKEYS:Array<keyofPublicCard>=['title','place','startAt','category','publicUrl'];exportfunctionminimize(source:Record<string,string>):PublicCard{constout:Record<string,string>={};for(constkeyofKEYS){consttext=source[key]?.trim();if(!text||text.length>128){thrownewError(`invalid public field:${key}`);}out[key]=text;}returnoutasunknownasPublicCard;}exportfunctionauditKeyCounts(source:Record<string,string>):number[]{consttotal=Object.keys(source).length;return[total,KEYS.length,total-KEYS.length];}代码的关键不在for循环,而在返回值是新建的PublicCard。如果以后源对象增加ticketSecret这样的字段,默认就不会穿过这层边界;即便产品决定增加新公开字段,也需要显式修改白名单和对应测试,避免代码更新后分享内容无声扩张。当然,Record<string,string>只是教学夹具的输入类型,真实产品中的报名列表、日期或地址可能是嵌套对象,应先在更靠近数据源的层完成类型转换,不要把任意对象强行转字符串再分享。
auditKeyCounts只可用来生成摘要,不该当成安全验证本身。假设同样是12个键,开发者错误地把privateToken换成新的公开键,数量依然可能是5/7;因此测试必须逐一断言最终对象的键集合恰好等于白名单,并对各字段做内容级验证。数量用于操作面板展示,键集合断言才是上线前的基本门禁之一。
这里为什么不做“某些字段为空时从另一个字段兜底”?例如没有公开场地简称时,若回退到精确街道地址addressExact,就突破了发布契约。遇到空值要停止导出,提示运营补齐公开内容,而不是用隐私字段偷偷填空。失败时输入仍留在受保护的业务数据层,导出层不保留半成品快照,避免用户修正后旧值混入新任务。
四、链接必须是产品认可的URL,不是能打开就行
第二段代码专门拦截危险的publicUrl。为了减少未证实的平台网络能力,本例不用系统打开网页,也不宣称URL实际在线。我们只做一个非常窄的应用级格式判定:必须是HTTPS,主机名精确为events.example.org,路径形如/agenda/42这样的正整数ID,不能带凭据、端口、查询、片段或控制字符。应用可以把这个校验当成分享前的输入门禁,但并不能因此证明服务端内容可信。
// model/PublicLinkRule.ets —— 本应用的严格URL规则,不是系统自动校验exportfunctionvalidatePublicUrl(url:string,eventId:number):string{constnormalized=url.trim();constmatch=/^https:\/\/events\.example\.org\/agenda\/([1-9][0-9]*)$/.exec(normalized);if(!match||Number(match[1])!==eventId){thrownewError('PUBLIC_LINK_REJECTED');}returnnormalized;}exportfunctiontryProbeBadLink(input:string,eventId:number):boolean{try{validatePublicUrl(input,eventId);returnfalse;}catch(_){returntrue;// 示例中计作一次被阻断的异常链接}}这段正则非常严格,适合当前只有一个已知URL模式的示例,却不适合通用浏览器。实际业务如果采用跳转、国际化路径或有签名参数,应该基于可信解析器建立规范化和签名验证流程,而不是无限往正则里追加分支。特别是https://events.example.org.evil.invalid/...一类相似域名必须被拒绝;只做startsWith('https://events.example.org')会留下明显风险。
本例使用eventId=42,所以https://events.example.org/agenda/42是允许值;恶意测试输入javascript:alert(1)被阻断并使unsafeLinkBlocked=1。这次计数发生在本地输入验证,不能写成UDMF系统返回了某种安全错误码。负责URL策略的人应该可以从日志看到PUBLIC_LINK_REJECTED和被拒原因类别,却不应在日志里复述整条未经校验的外部URL,更不要把其中可能包含的口令作为诊断信息。
五、以两条标准记录交出“最小公开视图”
当且仅当白名单与URL检查均通过,才创建UDMF对象。下面使用华为文档所列的uniformDataStruct.PlainText和uniformDataStruct.Hyperlink,用unifiedDataChannel.UnifiedRecord包裹两种标准内容,再加入UnifiedData。注意这是构造数据对象的示例,不是调用碰一碰系统传输接口,更不伪造一个不存在的“NFC发送成功”回调。
// model/ShareRepresentation.ets —— 仅构造UDMF对象,不发送到设备import{unifiedDataChannel,uniformDataStruct,uniformTypeDescriptor}from'@kit.ArkData';import{PublicCard}from'./CardWhitelist';exportfunctionbuildShareData(card:PublicCard):unifiedDataChannel.UnifiedData{constdescription=`${card.title}\n${card.startAt}\n${card.place}\n${card.category}`;constplain:uniformDataStruct.PlainText={uniformDataType:'general.plain-text',textContent:description,abstract:card.title};constlink:uniformDataStruct.Hyperlink={uniformDataType:'general.hyperlink',url:card.publicUrl,description:card.title};consttextRecord=newunifiedDataChannel.UnifiedRecord(uniformTypeDescriptor.UniformDataType.PLAIN_TEXT,plain);constdata=newunifiedDataChannel.UnifiedData(textRecord);data.addRecord(newunifiedDataChannel.UnifiedRecord(uniformTypeDescriptor.UniformDataType.HYPERLINK,link));returndata;}如果要读取结果,可在适合的测试层使用getRecords()核对记录数量和具体类型。不同API版本的记录提取方式存在差异,例如官方参考中出现getEntry与getTypes等接口;使用前须确认目标SDK版本的签名。不要从getRecords().length===2就推断业务上“隐私干净”,因为文本内容可能依然含有不允许的数据。正确断言必须从清理前后对象、最终两个字段实体与接收侧显示三个层面共同检查。
同样不能把UnifiedData和insertData混为一谈:构造对象在本地就可以完成,写入系统公共数据通路则是另一步,可能有能力、授权、设备和场景限制。第十二轮演示里representations=2表示两个本地标准记录,不是“接收端已读取两种格式”。这是一个较小但非常重要的命名选择,它让UI和用户知道任务还在准备阶段。
配图采用DevEco Studio白色主题的拟真演示布局,左工程目录包含ShareCardPage.ets、RedactionAuditPage.ets和两段模型文件;中央显示最小对象装配与白名单逻辑;最右模拟器展示卡片准备态;底部HiLog显示CARD-1010-12 source=12 allowed=5 redacted=7与representations=2 EXPORT_READY。它不是编译截图,不应据此推论UDMF发送路径、设备发现或权限配置已经完成。
六、把用户真正能看到的主页面收在准备阶段
ShareCardPage主页面只显示公开信息。即使应用持有原始源对象,UI也不能把7个敏感字段“先展示、再打码”:截图录制、辅助读屏和错误反馈都可能在视觉遮盖之外接触它们。更稳妥的做法是展示从minimize()返回的最小视图。页面标题“活动卡片分享”,活动event_talk_42,可见字段分别是城市开发者夜校、创智中心A厅、10月12日19:30、技术沙龙与公开URL。
主状态EXPORT_READY的语义是“两条UDMF记录已按策略准备,等待用户继续选择经验证的发送方式”。这里绝不自动弹出一个“成功碰一碰”的绿色勾,也不能根据页面上出现NFC图标就说系统完成了跨设备交付。按钮应该叫“检查分享内容”或“继续到系统分享”,而不是“对方已收到”。若正式实现接入系统分享,需要在授权后另外维护真实的SEND_STARTED、SEND_FAILED和SEND_CONFIRMED状态,这些不在当前模型结果中。
本轮模拟输入统计是12条字段、5条允许、7条移除、2个标准记录、1条恶意链接测试被拒。数字比较容易对齐,但更多重要的是时间关系:一旦用户切换到另一个活动,上一活动还在准备中的异步回调就不可以改写新页面;如果用户退出分享预览,也应该清除或隔离引用旧卡片的内存对象。尤其不能把一个UnifiedData实例长期当全局单例复用,因为它可能持有上一次卡片的内容。
这里还有一种“看着正确、实际不稳定”的情形。用户点击分享以后编辑了活动时间,如果纯文本记录基于修订21构造、超链接描述基于修订22构造,那么两条记录虽然各自合法,合并展示仍可能互相矛盾。实际工程应在清理完成时冻结一个不可变的PublicCard快照,让所有表示都从同一快照生成,并且仅在校验通过后暴露给系统投递层。
七、诊断要记录谁被删掉,而不是泄露删掉的值
RedactionAuditPage的职责是审查策略,不是回显敏感信息。示例中按名称列出7个被抹除的键:organizerPhone、mobile、email、internalNote、privateToken、attendeeList、addressExact;用摘要记录两条表示已生成。真正的手机号、邮箱、口令和具体报名名单既不写HiLog,也不出现在诊断图中。看到字段名,开发者就知道是哪个规则在工作;看到实际值反而是一种新的泄露面。
对一个本地坏链接测试,诊断页只写10:32:14 BLOCK unsafe scheme,不要显示完整攻击字符串。接着写10:32:15 KEEP publicUrl /agenda/42与10:32:16 BUILD 2 records EXPORT_READY。并且把“系统投递”专门标为NOT_RUN。这样排查时能够把数据构造成功、应用策略拦截和真正发送三个环节分开。如果只输出一行“share success”,哪怕真实业务出问题,也根本无法复盘到底成功的是哪一层。
预计的本地夹具用例不需要设备:一份合法12字段卡片应输出恰好5个安全字段;随机新增秘密键应保持输出不变;白名单字段缺失时必须拒绝;多一个公开字段也必须更新契约;坏URL应返回PUBLIC_LINK_REJECTED;最终对象有1条纯文本记录和1条超链接记录。以上是拟定的验收条件,在真实工程中需要以Node或ArkTS测试框架执行后才能说通过。
再进一步,边界不只在发送前。系统或接收端可能把超链接打开到浏览器,公开站点本身是否存在开放重定向、可追踪参数或过度授权仍是服务器问题。我们的URL规则能够阻止客户端传出明显非白名单的链接,却不能代替站点安全审计。用户授权页面、发送途径、传输协议、设备支持矩阵与隐私合规文案都应另列真实验收项,不要在一篇“分享前清理”的示例里暗示已经覆盖。
八、对象生命周期和失败补偿不能省略
这个演示没有把UnifiedData写入系统公共通路,因此释放责任主要是应用引用管理:取消预览后丢弃候选对象,避免保存在跨页面全局状态里;如果新卡片构建失败,继续显示旧卡片时必须清晰标记旧活动ID,否则用户会误以为修正后的数据已经生效。对于真正的系统通路写入与读取,应该根据官方接口处理可能出现的参数错误、服务异常、取消行为和清理范围,不能捏造一个通用的deleteAll()当作万能回滚。
假如数据共享动作由用户点击触发,最稳妥的入口是先冻结活动快照,再做白名单和URL验证,再构建两种表示,最后才允许调起实际系统能力。中间任意阶段失败,就让任务停在VALIDATION_HOLD或BUILD_HOLD并明确原因分类,而不是用“发送失败请重试”掩盖数据合同错误。重新进入页面还应恢复源业务数据的最新版本,而不是从上一回的UDMF记录反向回填用户私人报名表。
为什么这篇没有用HTML表示?HTML记录在某些富文本分享场景很有用,但如果本例仅需四行文字和一个HTTPS链接,额外HTML会引入标签转义、链接点击、嵌入内容和渲染差异的第二套风险。选择两个更简单的标准类型,是按实际产品场景收窄攻击面,而不是断言UDMF无法承载其他类型。之后真的要分享带格式的活动卡片,也应另设HTML生成器,仅从5个安全字段构建内容,并对每个文本节点执行编码,不能从运营原始富文本直接透传。
如果要把这套规则接入团队CI,建议每次白名单变更同时提交一份可审查的“公开字段说明”,由产品、安全与实现方分别核对。配置中的allowed=5不只是一个页面数字,而是一项发布合同:新增字段为什么公开、是否需要用户同意、接收方是否能正确解释,都应在变更记录里留下答案。这样的流程不会自动让应用合规,却能防止一个看似无害的UI重构把秘密键重新传到共享层。
本例得出的结果很克制:在给定夹具上,最小公开卡片可以由12个源字段收缩到5个,生成纯文本和超链接两个UDMF标准记录;1条坏链接测试应被应用策略拒绝,页面状态保持EXPORT_READY,真实发送明确NOT_RUN。这不是“精准碰一碰已成功”的证明。下一步要做的是把该最小对象接到华为当前支持的分享场景,对设备矩阵、系统权限、接收方渲染和用户撤销行为逐条验收,而不能把一张好看的成功态示意图当成接口行为证据。
官方核对入口:华为统一数据结构指南 https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V14/uniform-data-structure-V14 ,包括UnifiedData/UnifiedRecord;统一数据类型接口 https://developer.huawei.com/consumer/en/doc/harmonyos-references-V14/js-apis-data-uniformtypedescriptor-V14 说明PLAIN_TEXT、HYPERLINK等标准类型。精确的设备版本与实际投递能力须以目标SDK及当前官方对应场景文档为准。