【HarmonyOS 7新能力|036】分布式数字身份工程封装:把接入逻辑放进可维护的分层结构

数字身份场景常见的错误,是为了证明一个简单事实而传递整份身份资料:只需要确认"已成年",却上传姓名、证件号码和完整出生日期。分布式数字身份真正值得关注的工程价值,是让用户持有凭证,并在明确用途下只披露完成业务所需的声明。要实现这一点,项目不能把凭证读取、授权弹窗、证明生成和业务放行揉在一个页面中。
说明:本文使用的
IdentityOrchestrator、CredentialPort等均为教学抽象,不对应某个 HarmonyOS SDK 的真实类名或接口签名。DID、可验证凭证、密码学套件、设备支持及合规要求,请以当前官方文档、目标 SDK 与实际业务所在地规则为准。
1. 身份验证的输入应是声明,而不是证件
业务首先描述自己需要验证的事实,例如年龄满足范围、某资质有效、会员状态成立。请求中要包含用途、接收方、有效时间和是否允许替代声明,而不是预设必须取出哪张证件的哪些字段。
ts
export interface ClaimRequest {
requestId: string
verifierId: string
purpose: string
claims: readonly ClaimRequirement[]
expiresAt: number
nonce: string
}
export interface ClaimRequirement {
type: string
predicate?: string
required: boolean
}
这种建模把业务目标与凭证格式分开。未来更换凭证发行方或证明协议时,业务页面仍然只关心声明是否满足。
2. 四层架构约束数据流向

业务页面展示用途并接收验证结果;身份编排层校验请求、组织授权和选择凭证;能力适配层隔离钱包、证明与验证接口;凭证仓库管理索引、状态和受保护存储。上层只依赖抽象能力,凭证原文不应穿过所有层级。
ts
export interface CredentialPort {
findCandidates(requirements: readonly ClaimRequirement[]): Promise<CredentialSummary[]>
createPresentation(input: PresentationInput): Promise<PresentationResult>
}
export interface VerificationPort {
verify(presentation: string, request: ClaimRequest): Promise<VerifyResult>
}
端口应保持最小,避免把底层 SDK 对象直接返回页面。平台升级只修改适配器,授权规则和业务结果映射则留在编排层。
3. 请求进入后先做真实性和时效校验
应用不能因为请求长得像 JSON 就信任它。编排器应检查接收方标识、用途是否为空、时间窗口、随机数、声明白名单和请求完整性。来自二维码、网页或跨设备的输入都视为不可信边界。
ts
export function validateRequest(req: ClaimRequest, now: number): ValidationResult {
if (!req.requestId || !req.verifierId || !req.purpose) return invalid('MISSING_CONTEXT')
if (req.expiresAt <= now) return invalid('REQUEST_EXPIRED')
if (!req.nonce || req.claims.length === 0) return invalid('INVALID_CHALLENGE')
if (req.claims.some(item => !claimAllowList.has(item.type))) return invalid('CLAIM_NOT_ALLOWED')
return valid()
}
随机数与请求 ID 用于防止旧证明被简单复用。具体签名校验与信任链必须调用经过验证的平台或密码学实现,不能自创算法。
4. 用户授权必须具体、可理解
"是否同意授权"信息量不足。授权页需要清楚展示接收方、用途、这一次要证明的声明、是否会传出原始属性、有效期限和拒绝后的影响。可选声明默认不勾选,用户选择只对本次请求生效。
ts
export interface ConsentViewModel {
verifierName: string
purpose: string
disclosures: readonly DisclosureItem[]
expiresText: string
canDeny: boolean
}
授权记录与凭证内容分离,只记请求摘要、选择项和时间。不能把一次授权扩展为长期通用授权,也不能用拒绝后无限弹窗迫使用户接受。
5. 候选凭证选择遵守最小披露

同一声明可能由多张凭证满足。选择器应优先考虑披露属性更少、有效期合适、来源受信且状态可确认的候选,而不是简单拿第一张。算法输出选择理由,便于授权页解释和测试。
ts
export function rankCandidate(c: CredentialSummary, req: ClaimRequest): number {
const extraPenalty = c.availableClaims.filter(x => !requested(req, x)).length * 10
const expiryPenalty = c.expiresSoon ? 20 : 0
const trustBonus = c.issuerTrusted ? 30 : 0
return trustBonus - extraPenalty - expiryPenalty
}
示例权重仅说明方法,不是通用标准。若协议支持谓词证明,应优先证明结论;若只能披露属性,则明确告诉用户将发送什么。
6. 凭证摘要与凭证材料分开
列表页只需要名称、发行方、可用状态和到期提示,不需要解密完整凭证。仓库返回摘要,只有用户确认并进入证明生成时,受控适配器才在最短生命周期内访问必要材料。
ts
export interface CredentialSummary {
id: string
displayName: string
issuerName: string
availableClaims: readonly string[]
expiresAt: number
issuerTrusted: boolean
expiresSoon: boolean
}
UI 不缓存原始凭证,不把材料写入日志、剪贴板或普通首选项。离开流程后清理临时对象,系统截图和后台预览也要按产品安全策略处理。
7. 证明生成必须绑定本次用途
生成的证明应绑定请求随机数、验证方、用途和时效,避免在另一个场景被重放。编排器构造严格输入,适配器完成真实密码学操作,业务层不得拼接签名或自行管理私钥。
ts
export interface PresentationInput {
credentialId: string
requestedClaims: readonly string[]
verifierId: string
purpose: string
nonce: string
expiresAt: number
}
export type PresentationResult =
| { status: 'created'; payload: string }
| { status: 'cancelled' }
| { status: 'failed'; code: string }
失败只向页面暴露可处理的归一化错误,底层细节进入脱敏诊断。用户取消不是异常,也不应自动重试。
8. 验证结果要与业务决定解耦
密码学验证通过只说明证明在当前规则下成立,不代表业务一定要放行。业务还可能检查请求是否过期、声明是否完整、发行方是否符合场景策略以及凭证是否撤销。验证层输出事实,策略层做决定。
ts
export interface VerifyResult {
cryptographicallyValid: boolean
claimsSatisfied: readonly string[]
missingClaims: readonly string[]
issuerStatus: 'trusted' | 'unknown' | 'blocked'
credentialStatus: 'active' | 'revoked' | 'unknown'
}
unknown 不能被偷偷转为通过。低风险场景可提示补充,高风险场景可拒绝或转人工流程,规则必须明确记录。
9. 状态机避免重复授权与重放
身份流程至少包含 validating、selecting、consenting、presenting、verifying、completed、cancelled 和 failed。状态转换由编排器统一执行,页面按钮只发送事件。
ts
export type IdentityFlowState =
| { kind: 'validating' }
| { kind: 'consenting'; model: ConsentViewModel }
| { kind: 'presenting'; requestId: string }
| { kind: 'completed'; result: BusinessIdentityResult }
| { kind: 'failed'; code: string; retryable: boolean }
同一个 requestId 在进行中时拒绝再次启动,完成后保存短期消费标记。重复点击、页面旋转或返回前台不能触发第二次证明生成。
10. 撤销、过期和离线边界
凭证有生命周期。仓库在使用前检查本地到期状态,适配器按官方机制确认撤销状态。若撤销信息必须联网而当前离线,结果应为 unknown,由场景策略决定是否允许稍后验证。
ts
export interface CredentialStatusPolicy {
requireFreshStatus: boolean
maxStatusAgeMs: number
allowOfflineUnknown: boolean
}
缓存撤销状态要带获取时间和来源,不能永久复用。删除凭证、清除账号或撤销授权时,应同步清理索引、临时证明和相关缓存,但保留何种合规审计需单独定义。
11. 测试聚焦隐私不变量
除了正常流程,还要测试过期请求、未知声明、用户拒绝、无匹配凭证、证明失败、验证方不可信、凭证撤销、离线未知和重复请求。更重要的是验证"不该发生的事":可选属性没有默认披露、日志没有凭证原文、页面拿不到私钥、拒绝后不会继续生成证明。
ts
it('discloses only approved claims', async () => {
const input = buildPresentation(['ageOver18'])
const result = await adapter.createPresentation(input)
expect(result).not.toContain('fullBirthDate')
expect(result).not.toContain('documentNumber')
})
真机验收还需覆盖系统返回、后台恢复、锁屏、设备时间变化和权限拒绝。协议互操作性应与真实验证端联调,单元测试不能替代跨实现验证。
12. 落地清单与总结
落地前确认:业务请求的是声明而非整份证件;用途、接收方和时效可见;请求经过真实性校验;可选信息默认不披露;凭证材料只在受控层短暂访问;证明绑定本次随机数和验证方;验证事实与业务决策分离;撤销、过期、离线都有明确状态;日志与缓存不含敏感原文;用户可以拒绝和撤销。
分布式数字身份的工程目标不是把传统证件电子化,而是重新收紧身份数据的流动路径。通过声明驱动、明确授权、候选最小化、用途绑定和分层适配,项目才能在能力演进时保持边界稳定,让"只证明必要事实"成为代码能够持续验证的不变量。
本文为 HarmonyOS 7 新能力工程实践系列第 036 篇。示例用于说明架构方法,生产接入请依据官方文档、目标 SDK、协议规范和隐私合规评审执行。