
你好欢迎来到我的博客我是【菜鸟学鸿蒙】我是一名在路上的移动端开发者正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来也为了和更多同路人互相启发我决定把探索 HarmonyOS 的过程都记录在这里。️主要方向ArkTS 语言基础、HarmonyOS 原生应用Stage 模型、UIAbility/ServiceAbility、分布式能力与软总线、元服务/卡片、应用签名与上架、性能与内存优化、项目实战以及 Android → 鸿蒙的迁移踩坑与复盘。内容节奏从基础到实战——小示例拆解框架认知、专项优化手记、实战项目拆包、面试题思考与复盘让每篇都有可落地的代码与方法论。 我相信写作是把知识内化的过程分享是让生态更繁荣的方式。如果你也想拥抱鸿蒙、热爱成长欢迎关注我一起交流进步前言登录成功以后服务端通常会返回 access token、refresh token 或其他应用凭据。它们看起来也是“几个字符串”所以有些代码会顺手把它们和主题模式、字体大小、首次启动标记一起塞进 Preferences。从存储模型看这当然很方便从安全等级看却不是一回事。华为官方将 Preferences 定位为存储应用程序设置的轻量级数据库典型场景是字体大小、夜间模式等个性化配置Asset Store Kit 则专门提供用户短敏感数据的安全存储和管理能力官方列出的典型数据就包括账号密码、Token 类应用凭据以及其他关键明文。这次不展开讲整个安全体系只做一个可以迁移到实际项目里的最小场景把登录 Token 从“普通配置项”提升为关键资产并完整串起新增、查询、刷新和退出登录删除这条生命周期。HarmonyOS 7 已正式对应 API 26.0.0Asset Store 的核心 ArkTS 接口并不是 API 26 才出现它的首批接口从 API version 11 开始支持因此在 HarmonyOS 7 项目中仍然可以继续使用。一、Preferences 适合什么Token 又为什么不同Preferences 适合保存的是“小而简单的应用配置”。华为官方 FAQ 给出的典型例子包括字体大小、是否开启夜间模式等。它解决的是轻量配置数据的持久化问题而不是专门针对敏感凭据设计的安全存储。比如下面这些信息放在 Preferences 里很自然theme dark fontScale 1.1 hasShownGuide true defaultTab 2Token 则决定了另一件事持有它的人在一定条件下可能可以代表当前登录用户访问服务端资源。所以真正需要区分的不是“它是不是字符串”而是这个值泄露以后是否会形成身份、权限或敏感数据风险。如果答案是会那么它就不应该仅仅因为存取方便而被当成普通配置。Asset Store Kit 正是针对这一类短敏感数据提供的能力。官方当前文档中Asset Store Kit 被归在系统安全能力下并提供 ArkTS 与 C APIArkTS 当前对应ohos.security.asset推荐通过kit.AssetStoreKit导入。二、先把 Asset Store 的边界弄清楚在开始写代码前有几条官方规则最好先确认。项目当前结论KitAsset Store KitArkTS 模块asset推荐导入import { asset } from kit.AssetStoreKit;首批支持版本API version 11系统能力SystemCapability.Security.Asset适用数据密码、Token、应用凭据等短敏感数据同步 APIAPI version 12 起提供对应同步接口HarmonyOS 7HarmonyOS 7 对应 API 26.0.0可继续使用该能力Asset Store 不是“大文件加密盘”也不适合随意塞入整份 JSON、图片或者业务数据库。以关键资产明文SECRET为例当前官方开发指导对其长度存在明确限制适合的仍然是 Token、密码这类短数据。还有一个非常重要的概念一条 Asset 并不只有“值”。它通常由几类属性组成SECRET 真正的关键资产明文 ALIAS 这条资产的唯一索引 ACCESSIBILITY 与锁屏状态有关的访问条件 AUTH_TYPE 是否需要用户认证 DATA_LABEL_* 业务自定义附属信息 ……后面很多容易写错的地方其实都来自把这些字段误认为“都是加密后的业务字段”。并不是。三、设计登录 Token 的 alias为了让示例足够聚焦这里只保存一条 access token。可以为它设计一个固定 aliaslogin.access_token如果应用允许多个账号同时保留登录态也可以让 alias 带上业务侧账号标识login.access_token.accountId但这里有一个原则不要把 Token 本身拼进 alias。官方文档明确指出ALIAS是关键资产的唯一索引同时它属于业务自定义信息在当前开发指导中官方特别提醒ALIAS以及DATA_LABEL_*一类附属信息并不是天然应该承载敏感个人数据的字段。因此 alias 应该承担“定位资产”的职责而不是“再存一份秘密”。本例统一使用constACCESS_TOKEN_ALIAS:stringlogin.access_token;四、搭一个最小 TokenStore下面的代码只做四件事保存 Token查询 Token更新 Token删除 Token。字符串与Uint8Array之间使用 ArkTS Kit 中的TextEncoder和TextDecoder转换。import{asset}fromkit.AssetStoreKit;import{util}fromkit.ArkTS;constACCESS_TOKEN_ALIAS:stringlogin.access_token;functionstringToBytes(value:string):Uint8Array{constencodernewutil.TextEncoder();returnencoder.encodeInto(value);}functionbytesToString(value:Uint8Array):string{constdecoderutil.TextDecoder.create(utf-8,{ignoreBOM:true});returndecoder.decodeToString(value,{stream:false});}这里真正需要关注的是Asset 的值类型不是普通stringAssetMap中字节类属性使用Uint8Array。1. 登录成功后添加 TokenexportasyncfunctionsaveAccessToken(token:string):Promisevoid{constattributes:asset.AssetMapnewMap();attributes.set(asset.Tag.SECRET,stringToBytes(token));attributes.set(asset.Tag.ALIAS,stringToBytes(ACCESS_TOKEN_ALIAS));attributes.set(asset.Tag.ACCESSIBILITY,asset.Accessibility.DEVICE_FIRST_UNLOCKED);awaitasset.add(attributes);}SECRET才是真正保存 Token 的位置。ALIAS用来唯一定位这条资产。这里把ACCESSIBILITY设置为asset.Accessibility.DEVICE_FIRST_UNLOCKED官方当前定义的三种锁屏访问控制包括DEVICE_POWERED_ON DEVICE_FIRST_UNLOCKED DEVICE_UNLOCKED它们分别表示开机后可访问设备首次解锁后可访问设备当前处于解锁状态时才可访问。对于普通登录 Token本例选择DEVICE_FIRST_UNLOCKED意味着设备重启后在用户完成一次解锁之前不能读取首次解锁后即使之后再次锁屏访问条件也与DEVICE_UNLOCKED不同。这只是示例策略不代表所有 Token 都必须选择它。实际项目应根据“后台是否仍需要访问 Token”“是否希望锁屏立即阻断访问”等业务安全要求决定。五、查询 Token最容易漏掉 RETURN_TYPE查询看起来很简单按 alias 找到那条 Asset。但这里恰好有一个华为官方 FAQ 已经专门解释过的问题——如果要读取关键资产明文查询参数需要指定RETURN_TYPE ReturnType.ALL。否则可能出现“Asset 明明存进去了但取不到 SECRET”的情况。exportasyncfunctiongetAccessToken():Promisestring|undefined{constquery:asset.AssetMapnewMap();query.set(asset.Tag.ALIAS,stringToBytes(ACCESS_TOKEN_ALIAS));query.set(asset.Tag.RETURN_TYPE,asset.ReturnType.ALL);constresult:Arrayasset.AssetMapawaitasset.query(query);if(result.length0){returnundefined;}constsecretresult[0].get(asset.Tag.SECRET);if(!(secretinstanceofUint8Array)){returnundefined;}returnbytesToString(secret);}ReturnType当前有两个核心值ALL ATTRIBUTES其中官方说明ALL返回关键资产明文及属性查询单条关键资产明文时使用ATTRIBUTES只返回属性不包含关键资产明文用于批量查询资产属性。这里比较容易理解错。asset.query()成功执行只说明查询操作本身没有抛错并不意味着你一定要求系统把SECRET返回给你。这也是为什么排查 Asset 查询问题时RETURN_TYPE应该放在非常靠前的位置。六、Token 刷新时不必先删再存Token 有一个很常见的生命周期登录 ↓ 得到 access token ↓ 使用一段时间 ↓ 服务端刷新 token ↓ 替换旧 token如果 Asset 已经存在再次使用相同 alias 调用普通新增操作可能产生资产重复冲突。官方接口提供了更新关键资产能力因此更自然的模型是alias 不变 SECRET 更新也就是“身份不变秘密值发生变化”。按官方接口模型可以组织成下面这种代码exportasyncfunctionupdateAccessToken(newToken:string):Promisevoid{constquery:asset.AssetMapnewMap();query.set(asset.Tag.ALIAS,stringToBytes(ACCESS_TOKEN_ALIAS));constattributesToUpdate:asset.AssetMapnewMap();attributesToUpdate.set(asset.Tag.SECRET,stringToBytes(newToken));awaitasset.update(query,attributesToUpdate);}官方当前更新指导明确将更新操作拆成两组属性query attributesToUpdate前者定位需要修改的资产后者放入新值SECRET属于允许更新的资产内容。这个模型对 Token 很合适因为 Token 刷新并不意味着产生了一种新的本地资产。七、退出登录时删除的不只是“内存登录态”退出登录通常至少有两层动作1. 清理当前内存中的用户状态 2. 清理持久化登录凭据如果第二步漏掉界面虽然已经回到登录页设备里却还保留着旧 Token。因此可以明确设计一个本地凭据销毁动作exportasyncfunctionremoveAccessToken():Promisevoid{constquery:asset.AssetMapnewMap();query.set(asset.Tag.ALIAS,stringToBytes(ACCESS_TOKEN_ALIAS));awaitasset.remove(query);}这样完整生命周期就变成登录成功 ↓ asset.add() ↓ 业务请求时读取 ↓ asset.query() ↓ Token 刷新 ↓ asset.update() ↓ 主动退出登录 ↓ asset.remove()如果应用还有 refresh token建议把它设计成另一条独立资产例如login.access_token login.refresh_token这样比把两个值序列化进一个 JSON 再整体存储更容易独立管理生命周期。八、锁屏访问控制到底控制什么ACCESSIBILITY很容易被理解成普通的“文件权限”。实际上它表达的是关键资产在什么设备锁屏状态下允许被访问。官方定义如下Accessibility含义DEVICE_POWERED_ON开机后可访问DEVICE_FIRST_UNLOCKED本次开机首次解锁以后可访问DEVICE_UNLOCKED仅设备处于解锁状态时可访问官方还说明如果设备没有设置锁屏密码DEVICE_FIRST_UNLOCKED和DEVICE_UNLOCKED会退化为开机后可访问。因此不要把asset.Accessibility.DEVICE_UNLOCKED理解成“每次查询 Asset 都会弹出生物识别。”这是两套概念。Accessibility是基于锁屏状态的访问控制。如果要进一步要求“访问关键资产前进行用户认证”Asset 还提供了AUTH_TYPE等相关机制。官方当前AuthType中包含NONE与ANY其中ANY表示 PIN、人脸、指纹等任一种用户认证通过后可访问。本篇只讨论普通登录 Token不把用户认证流程继续展开否则还需要引入preQuery、用户认证以及postQuery的完整过程。九、DATA_LABEL 不是另一个 SECRET这是 Asset Store 里很值得单独强调的一点。开发者看到SECRET ALIAS DATA_LABEL_NORMAL_1 DATA_LABEL_CRITICAL_1很容易直觉上认为既然这些字段都属于 Asset那我把 refresh token 放DATA_LABEL_NORMAL_1应该也安全。这种理解不对。华为当前官方开发指导明确提醒ALIAS与名称包含DATA_LABEL的业务自定义附属信息不能简单等同于关键资产明文本身相关字段是否加密、是否具有完整性保护都有独立定义。官方在更新关键资产指导中直接提示这类业务自定义信息不要随意存放敏感个人数据。所以最稳妥的职责划分是SECRET └── 真正的 Token / 密码 / 应用凭据 ALIAS └── 非敏感唯一索引 DATA_LABEL_* └── 用于筛选、业务分类等附属信息API version 14 起还增加了与附属属性加密相关的REQUIRE_ATTR_ENCRYPTED能力但它也不能改变字段职责本身真正的核心敏感值仍然应该放在SECRET。十、默认场景下不需要额外声明持久化权限还有一个容易产生误解的问题用了 Asset Store是不是一定要在module.json5中增加权限对于本文这个普通 Token 场景没有设置资产的asset.Tag.IS_PERSISTENT因此不需要因为本文代码额外声明ohos.permission.STORE_PERSISTENT_DATA官方 API 文档明确说明只有设置Tag.IS_PERSISTENT时才涉及申请ohos.permission.STORE_PERSISTENT_DATA。也就是说不要为了“保险”把不需要的权限先全部申请上。IS_PERSISTENT主要涉及另一个问题应用卸载以后是否还保留关键资产。而登录 Token 恰好通常不应该随应用卸载继续保留所以本文没有开启这一能力。华为官方 FAQ 也提醒Asset 资产与设备相关并不是自动与业务账号绑定应用是否在卸载时保留关键资产也会影响之后能否查询到。十一、把整个登录凭据生命周期真正串起来有了前面的四个操作以后业务层可以把 Token 生命周期写得很清楚exportclassLoginCredentialStore{staticasynconLoginSuccess(accessToken:string):Promisevoid{awaitsaveAccessToken(accessToken);}staticasyncgetTokenForRequest():Promisestring|undefined{returngetAccessToken();}staticasynconTokenRefreshed(newAccessToken:string):Promisevoid{awaitupdateAccessToken(newAccessToken);}staticasynconLogout():Promisevoid{awaitremoveAccessToken();}}实际工程里还需要考虑重复登录等业务分支。例如登录成功时如果无法确定资产是否已经存在可以根据业务状态选择更新或者使用 Asset 提供的冲突处理机制。当前ConflictResolution提供OVERWRITE THROW_ERROR默认设计应由业务明确决定是覆盖已有资产还是把重复写入当成异常而不是在 catch 中把所有错误都吞掉。更重要的是不要把 Token 打进日志。例如下面这种日志即使只用于调试也不应该保留console.info(accessToken ${token});Asset Store 解决的是“落盘以后怎么安全保存”它并不能替你解决 Token 在日志、剪贴板、埋点、异常信息或者网络传输中被主动泄露的问题。十二、几个容易踩偏的地方1. Asset 查到了但为什么没有 SECRET优先检查query.set(asset.Tag.RETURN_TYPE,asset.ReturnType.ALL);这是华为官方 FAQ 已明确记录的问题。2. 同一个 alias 再次 add 报错alias 是资产唯一索引。如果 Token 已存在而服务端只是刷新了值优先考虑update而不是再增加一条同 alias 的资产。3. 锁屏后查询失败检查保存资产时使用的asset.Tag.ACCESSIBILITY接口错误码中存在24000005即锁屏状态与资产要求不匹配。4. 把敏感字段塞进 DATA_LABEL不要默认认为 AssetMap 里的所有字段都是同一种安全等级。真正的 Token 放进asset.Tag.SECRET5. 误以为资产自动跟业务账号走华为官方 FAQ 明确说明关键资产并不是自动和业务账号绑定而与设备存在关联。业务账号与 asset alias 之间的映射仍然需要应用自己设计。十三、实际项目可以按这个顺序排查遇到“Asset 存不进去”“Token 读不到”“锁屏后行为不对”时可以按下面的顺序定位确认 API 与 SDK 版本。Asset 首批 ArkTS 接口从 API 11 开始HarmonyOS 7 当前对应 API 26.0.0。确认 alias 是否一致。写入、查询、更新、删除必须定位到同一条资产。确认 SECRET 是否真的放在Tag.SECRET。查询明文时确认RETURN_TYPE是否为ReturnType.ALL。确认ACCESSIBILITY与当前设备锁屏状态是否匹配。检查是否误用了IS_PERSISTENT。如果使用需要再核对相应权限及卸载保留策略。最后再检查业务层登录状态。Asset 能成功查询不等于服务端 Token 一定仍然有效Token 过期、撤销等仍应根据服务端协议处理。开发经验总结Asset Store 真正有价值的地方不是把 Preferences 换成另一个 KV API而是让开发者在设计数据时明确区分“普通设置”和“关键资产”。对于登录凭据可以抓住四点Token、密码、应用凭据属于 Asset Store 官方明确覆盖的短敏感数据场景SECRET保存真正的敏感值ALIAS只负责唯一定位读取关键资产明文时需要正确设置RETURN_TYPEToken 的本地生命周期应与登录状态一致登录保存、刷新更新、退出删除。如果项目里现在还存在preferences.put(token, token)这段代码本身就是一个很值得重新审视的数据分类点。安全存储最先要解决的往往不是“用哪个 API”而是先回答这条数据到底是什么级别的数据。 写在最后如果你觉得这篇文章对你有帮助或者有任何想法、建议欢迎在评论区留言交流你的每一个点赞 、收藏 ⭐、关注 ❤️都是我持续更新的最大动力我是一个在代码世界里不断摸索的小码农愿我们都能在成长的路上越走越远越学越强感谢你的阅读我们下篇文章再见✍️ 作者菜鸟不学编程 本文原创转载请注明出处。