这篇我想聊一个很容易被低估的话题:很多应用不是没有“安全功能”,而是没有把“认证、授权、资产存储、访问审计”真正串起来。表面看只是多调几个接口,实际做工程时,它更像一条完整的安全访问链。

一、敏感数据保护,难点从来不是“加密”两个字

做账号系统、云服务接入、本地密钥管理时,大家都会想到“要安全”。

但真进入业务,安全不是一个抽象词,而是几个很具体的问题:

  • API 密钥要不要落本地?
  • 登录凭据和普通缓存数据能不能放在一起?
  • 每次读取敏感内容都要求用户认证,会不会太打断体验?
  • 哪些操作必须做指纹 / 人脸 / 锁屏口令验证?
  • 认证成功后,访问窗口应该维持多久?
  • 访问过哪些关键资产,需不需要留痕?

这些问题如果不在设计阶段想清楚,后面很容易出现两种极端:

一种是为了方便,所有数据都能直接读;
另一种是为了安全,所有操作都强行二次验证,结果把体验做崩。

所以我后来给自己定了一个很实用的拆法:

  1. User Authentication Kit 负责“你是不是本人”;
  2. Asset Store Kit 负责“资产放哪儿、怎么存、怎么读”;
  3. 业务层策略 负责“什么场景必须认证、什么时候允许继续访问”。

二、先分清“普通数据”和“关键资产”

我做这类功能时,不会把所有数据都扔进一个统一存储里。

因为“配置项”“页面缓存”“用户偏好”跟“API 密钥”“第三方登录凭据”“本地加密数据”完全不是一个安全级别。

我通常会这样分:

  • 普通数据:Preferences、数据库缓存、页面表单中间态;
  • 重要数据:账户标识、业务状态快照、最近操作记录;
  • 关键资产:API_KEY、refresh token、加密材料、本地受保护文档索引。

真正需要 Asset Store 保护的,应该是第三层。

这件事如果前期不分,后面越做越乱。因为安全策略一定是跟“数据价值”绑定的,而不是跟“代码写在哪个页面”绑定的。

三、认证入口别写散,敏感操作前做统一校验

最怕的一种写法,是每个按钮里自己去判断要不要认证。

这种写法短期很快,长期会很难维护。你今天有“查看 API 密钥”,明天又有“导出本地凭据”,后天还有“删除关键资产”。如果每个操作都自己判断,认证逻辑马上到处开花。

我更建议把“敏感操作前认证”做成一层统一服务。

例如下面这段代码,核心不是 API 写法,而是把认证动作从页面按钮里抽出来:

import userAuth from '@ohos.userIAM.userAuth';

export class AuthService {
  async authenticateForSensitiveAction(): Promise<boolean> {
    try {
      const result = await userAuth.getUserAuthInstance().auth({
        challenge: new Uint8Array([1, 2, 3, 4]),
        authType: [
          userAuth.UserAuthType.FINGERPRINT,
          userAuth.UserAuthType.FACE,
          userAuth.UserAuthType.PIN
        ],
        authTrustLevel: userAuth.AuthTrustLevel.ATL2
      });
      return result.result === 0;
    } catch (err) {
      console.error(`authenticate failed: ${JSON.stringify(err)}`);
      return false;
    }
  }
}

这个思路的好处很明显:

  • 哪些认证方式可用,由认证服务统一决策;
  • 页面只关心“是否通过”;
  • 后续切换认证策略,不用满工程改按钮事件。

四、Asset Store 不只是“存一下”,更重要的是访问边界

很多人会把安全存储理解成“把字符串存进去,再取出来”。

这理解不算错,但太浅了。

在工程里,Asset Store 更大的价值是:给关键资产一个独立、受控、可组合认证策略的存取空间。

像 API 密钥、登录凭据这类信息,不该和普通配置一起暴露在宽松访问链路里。

我一般会把资产层再封成单独服务,例如:

import asset from '@ohos.security.asset';

export class AssetService {
  async saveApiKey(value: string) {
    const attrs = new Map<string, Uint8Array | number>();
    attrs.set(asset.Tag.ALIAS, new TextEncoder().encode('api_key'));
    attrs.set(asset.Tag.SECRET, new TextEncoder().encode(value));
    attrs.set(asset.Tag.ACCESSIBILITY, asset.Accessibility.DEVICE_POWERED_ON);
    await asset.add(attrs);
  }

  async queryApiKey(): Promise<string> {
    const query = new Map<string, Uint8Array>();
    query.set(asset.Tag.ALIAS, new TextEncoder().encode('api_key'));
    const result = await asset.query(query);
    return new TextDecoder().decode(result.secret as Uint8Array);
  }
}

这里真正关键的是:资产访问不直接暴露给页面,而是放在统一服务层,由认证服务作为前置门禁。

也就是说,读取关键资产之前,应该先问一句:当前操作有没有通过身份验证?

这张 DevEco 图很适合拿来讲结构。左边目录把 AuthService、AssetService、页面组件分开,中间代码能看到认证与资产操作的衔接,右边模拟器则把“身份验证后访问资产”的实际交互直观展现出来。底部日志又把认证开始、认证成功、资产读取成功串起来了。

五、界面设计要把“认证状态”和“资产状态”分开展示

如果页面只给一个“安全中心”大按钮,读者其实很难理解整个链路。

我更喜欢把信息拆开:

  • 可用认证方式:指纹 / 人脸 / 锁屏口令;
  • 敏感操作说明:为什么要认证;
  • 关键资产列表:有哪些内容受保护;
  • 认证后状态:当前是否已授权访问;
  • 安全记录:最近的访问结果。

这张运行图就是一个比较典型的“认证前”状态:

  • 页面先告诉你支持哪些系统级认证方式;
  • 再告诉你为什么这次操作需要身份验证;
  • 下面列出关键资产,但默认不直接把内容全部暴露出来;
  • 只有用户明确发起“验证身份并访问”,才会进入下一步。

这种交互的好处是克制。它不会一上来就把敏感数据摊给用户,也不会让安全能力藏得太深。

六、真正好用的,不是认证成功,而是“认证成功后能形成一次完整授权会话”

很多实现到“认证成功”就结束了,但真实业务里还差一段。

用户认证成功之后,系统应该让这次访问形成一个清晰的授权结果:

  • 本次认证方式是什么;
  • 授权开始时间是什么;
  • 当前允许访问哪些资产;
  • 资产存储状态是否正常;
  • 最近有哪些关键操作已经发生。

这张图就比较像“认证后”的结果页:

  • 顶部直接告诉你身份认证成功;
  • 中间把 Asset Store 的状态、关键资产数量、认证保护状态列清楚;
  • 下方资产列表展示当前可访问项;
  • 最近安全记录把“读了什么、什么时候读的、结果如何”交代明白。

这样做的好处是,安全能力不再是一个黑盒。用户完成认证后,既知道自己做了什么,也知道系统开放了什么范围。

七、这类能力真正值钱的地方,是把安全体验做成“工程结构”

我自己的感受是,HarmonyOS 这类系统能力真正好用的地方,不只是 API 本身,而是它让你有机会把一套安全访问流程收成可维护的结构。

我现在会默认坚持几条原则:

  • 关键资产和普通数据一定分层;
  • 敏感操作前统一经过认证服务;
  • 认证方式尽量复用系统能力,不自己造轮子;
  • 页面展示“认证状态”和“资产状态”时要分层表达;
  • 关键操作要留记录,方便排查与审计;
  • 安全链路要兼顾体验,不能把用户每一步都挡住。

说到底,这篇文章不是想把安全讲得多玄,而是想强调一个非常工程化的结论:

安全不是某一个 API,也不是某一个页面,它是一条从身份确认、授权控制到资产访问与记录留痕的完整链路。

如果这条链路做顺了,你的应用不只是“更安全”,而是会明显更可维护。后面无论接密钥管理、账号安全、离线加密数据,结构都能接得住。

Logo

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

更多推荐