这两年做 Flutter 鸿蒙化改造我见过太多团队把“编译通过”当成“适配完成”。普通 UI 组件库确实如此但你一旦碰上 at_commons 这种把安全模型写进协议层的三方库就完全不是一回事了。at_commons 是 AT 协议生态里的公共基础库负责 atSign 身份体系、加密凭据管理、分布式数据签权访问这些底层能力。换句话说它是整个分布式通信系统的“地基”。地基要迁到 HarmonyOS 上不是把 Dart 代码编译过了就行还要把零信任模型里最关键的两件事——隐私凭据的加密轮转、复杂交互场景下的确权——原原本本落到鸿蒙的运行时里。这篇文章不打算讲“鸿蒙是什么”这种基础问题重点讲我在把 at_commons 适配到 HarmonyOS 过程中拆过的底层机制、踩过的坑以及最终的工程实现方案。如果你也在做 Flutter 库的鸿蒙化尤其是涉及密钥管理、认证授权、分布式通信这类安全敏感组件这篇应该能帮你省不少弯路。1. 先搞清楚at_commons 在分布式通信里到底承担什么角色其实很多团队在动手适配前根本没有完整读过这个库的代码。只是搜到某个依赖了 at_commons 的项目发现编译报错就丢给原生开发去处理。结果原生开发用 ArkTS 重写了一下午发现业务还是跑不通。这里有个非常关键的认知at_commons 不是普通的“工具集合”库。它属于 AT 协议体系。这个协议的核心思想是“每个人拥有自己的数据身份”每个用户在网络里用一个 atSign类似 username作为全局唯一标识数据存放在自己的个人数据服务器上。而 at_commons 就是在 Dart/Flutter 层把这种身份注册、加密认证、数据访问控制的能力抽象成通用接口。你可以把它理解为整个分布式应用的操作系统层——所有上层业务凡是涉及到“我是谁”“我能看什么”“我的数据怎么加密”最终都会跑到这一层来。所以它跟普通第三方库的根本区别在于普通库的鸿蒙化只需要解决“API 映射”而 at_commons 的鸿蒙化要解决“安全语义的迁移”。API 映射是功能层面的安全语义迁移则是信任模型层面的。API 映射错了顶多某个功能不可用安全语义迁移错了表面上功能都能跑但认证可能被绕过、密钥可能泄露、权限边界可能被突破这在零信任模型里是致命的。我在做适配时把整个任务拆成了三块这三块也是后面所有改造的主线密钥全生命周期管理包括密钥生成、存储、读取、轮转、销毁。网络通信层的安全底座包括 TLS 校验、自定义证书链处理、长连接通道。确权交互逻辑包括挑战-响应认证、数据共享授权、元数据校验。这三个部分恰恰都是鸿蒙原生能力跟 Android/iOS 差异最大的地方。普通的三方库鸿蒙化可能只需要在 ohos 目录下补一个插件实现at_commons 的鸿蒙化则是要把上面三块分别做一层“语义等价”的重新实现。我后面会逐个拆开讲。2. 凭据加密轮转的底层实现以及鸿蒙化之后的差异点2.1 AT 协议里的密钥体系不是一把钥匙走天下先说基础知识。AT 协议里每个 atSign 至少管理两类密钥签名密钥对和加密密钥对。签名密钥对用于身份认证加密密钥对用于数据加密。两者分开避免用同一把私钥既做身份证明又做数据解密这算安全模型里最基本的风险隔离。数据加密的实际流程是每个数据项data item先用一个随机生成的对称密钥加密这个对称密钥再用接收者的公钥做非对称加密最后和数据本体一起存到分布式数据存储里。这样设计的好处是接收者用私钥解开对称密钥再用对称密钥解开数据不需要给每份数据单独分配一组非对称证书。这个模式在业界有个很形象的名字叫“数字信封”外层是非对称加密的公钥锁内层是真正加密数据的对称密钥。这套机制到了鸿蒙上第一道坎就是密钥生成与存储接口不一致。原库在 Android 上用系统 Keystore在 iOS 上用 Keychain而鸿蒙上则要使用系统库提供的密钥管理能力例如 HUKSHarmonyOS Universal KeyStore。虽然概念类似但接口完全不同。如果你不想让 Dart 层每种平台写一套逻辑就必须用一个统一的 CryptoService 类把差异封装起来。我当时的封装方式是定义一个抽象接口包含 generateKeyPair、sign、verify、encrypt、decrypt、rotate 这些方法在 Android 和 ohos 各自实现。abstract class CryptoService { FutureKeyPairInfo generateKeyPair(String atSign, KeyType type); FutureUint8List sign(String keyId, Uint8List data, HashAlgorithm hash); Futurebool verify(String keyId, Uint8List data, Uint8List signature); FutureUint8List encrypt(String keyId, Uint8List plaintext); FutureUint8List decrypt(String keyId, Uint8List ciphertext); Futurevoid rotate(String atSign, KeyType type); }接口统一之后Dart 层原有的业务逻辑基本不用动只要在平台初始化时传入不同的实现即可。这一步是整个鸿蒙化改造的基础也是后面所有安全操作能跨端复用的前提。2.2 轮转为什么要“加密”且“轮转”“轮转”这个词其实大家都会说但真正实现的时候才知道细节很多。轮转不是简单生成一对新密钥然后把旧密钥删掉。在零信任模型下要保证三个目标旧密钥失效后存量数据仍然可读新数据从某个时间点开始全部使用新密钥已经共享出去的旧密钥信封需要被重新封装。具体流程一般是这样生成新的签名密钥对和加密密钥对。将新密钥写入密钥库并标记为新版本。遍历本地索引找出所有仍用旧加密公钥封装的数据项。用旧私钥解开数据密钥再用新公钥重新封装数据密钥写回。把旧密钥的状态标记为“退役”但保留一段时间用于解密历史数据。向网络内已知的关联 atSign 节点广播公钥变更信息。这个流程里最容易出问题的是第 4 步。数据量大的时候重新封装不是瞬间完成的如果在轮转中间有新的写入请求进来极容易出现“旧密钥封锁了新写入”的竞态。关于这个我后面实测部分会专门讲这里先提一个设计原则轮转必须设计成“渐进式”的不能设计成“开关式”的。渐进式意味着旧密钥和新密钥在一段时间内是共存的数据项通过信封头的 keyVersion 字段来区分用哪个密钥等到所有存量数据都迁移到新密钥之后旧密钥才真正进入退役状态。开关式则是新旧密钥的切换点非常明确切换之后所有旧数据立刻失效。后者在概念上很干净但在真实分布式环境里几乎不可用因为客户端不可能在同一时刻完成升级。2.3 鸿蒙 HUKS 与 Dart 层的配合方式鸿蒙侧的密钥操作基本走 HUKS。HUKS 支持非对称密钥的生成、导入、签名、验签、加密、解密但它有个特点密钥的私钥部分一旦生成应用层拿不到明文。这个理念跟 iOS Keychain 很像私钥只能在系统内使用优势是安全性高劣势是如果原库代码里有逻辑需要把私钥在 Dart 层做序列化处理就必须改掉。我建议的做法是原生侧把与密钥相关的操作全部封装成孤立的原子能力Dart 层只传 keyId 和待处理数据而不接触私钥。例如 sign 方法只接收 keyId、hash 算法标识、摘要数据返回签名结果decrypt 方法只接收 keyId、密文返回明文。这样设计还有一个额外的好处Dart 层永远不会因为误操作把私钥打印到日志里也不会在异常上报时把密钥字段带出去。我在审查代码时最怕看到的就是“为了方便调试把私钥序列化成 Base64 传到 Dart 层”这在安全审计里属于一票否决的问题。HUKS 强制私钥不落地反而帮你堵死了这个口子。3. 零信任确权链路从 PKAM 认证到数据授权访问的重构3.1 确权到底“确”的是什么很多开发者第一次听到“确权”会觉得抽象。其实在分布式通信场景里确权就是回答一个问题“访问这份数据的这个人是不是他声称的那个身份并且有没有权限看这份数据。”两个条件缺一不可。你光有权限但身份对不上不行你身份是真的但权限不够也不行。at_commons 里的确权链路分为两层身份确认通常采用 PKAMPublic Key Authentication Module核心是挑战-响应认证。服务端给客户端一个随机挑战字串客户端用自己的签名私钥对挑战字串签名服务端用客户端的公钥验签。验签通过说明持有者确实拥有对应的私钥。权限确认确认身份之后还要检查数据项的元数据包括共享列表、授权策略、有效期等。例如一个数据项可能只对指定 atSign 列表内的对象可见或者在某个时间点之后自动失效。零信任在这里体现得非常明显不管是首次连接还是已经保持很久的会话每一个数据请求都要走完整的认证和授权检查不存在“连上了就信任”的情况。我自己在适配前也有一个误解以为零信任主要是网络层面的东西做完才发现它在应用层协议里的体现更彻底——每一行读取、每一次写入都要过一遍确权。3.2 鸿蒙上的改造把认证链路从“Dart 全流程”改成“Dart 调度、原生执行”原库在移动端跑的时候很多敏感操作是依赖系统级组件的但接口设计上还有一部分可以在 Dart 层完成。鸿蒙化面临的问题是HUKS 不提供私钥导出所以涉及私钥的运算必须放在原生侧执行。因此我建议把原先集中式的认证代码重构成一个异步状态机。下面是认证阶段的状态流转init创建 ATClient 实例读取本地持久化的 atSign 身份信息。requestChallenge向远程服务端请求一次性挑战字串。signChallenge调用原生侧 HUKS 的 sign 接口对挑战字串做签名。sendResponse把签名结果和当前公钥指纹publicKeyFingerprint发送到服务端。verified服务端验签通过后交换一次性会话密钥后续通信使用会话密钥。我在重构时没有把整个状态机搬到原生侧而是保留了 Dart 层的调度逻辑只把涉及私钥的计算剥离开。这样做的好处是上层业务代码不用改认证时序、超时、重试仍然由 Dart 控制原生侧只负责“给什么算什么”的纯运算。代码上大概长这样class AuthStateMachine { final CryptoService _crypto; FutureAuthResult authenticate(AtSign atSign, RemoteNode node) async { final challenge await node.requestChallenge(atSign); final signature await _crypto.sign( atSign.signingKeyId, utf8.encode(challenge), HashAlgorithm.sha256, ); final response await node.sendAuthResponse(atSign, signature); if (response.ok) { return AuthResult.verified(sessionKey: response.sessionKey); } throw AuthException(challenge-response verification failed); } }3.3 数据授权访问把“能不配得”拦截在边界上权限确认这部分原库主要依赖数据项的 metadata。metadata 里记录了这个数据项的 owner、sharedWith 列表、访问策略等。比较关键的改造点是metadata 的读取和校验必须在同一个请求事务里原子完成不能先读数据本体再查权限否则会有时间窗口被利用。我在鸿蒙适配中增加了一个“授权边界检查层”。在这个检查层里Dart 侧先向本地的权限服务请求一个授权令牌只有拿到授权令牌之后才允许发起真正的数据读取请求。原生侧再对令牌做一次独立校验。虽然看起来多了一次调用但在零信任模型里这属于必要的冗余因为任何一层被绕过都可能造成越权访问。实际编码时我推荐在 at_commons 对外暴露的接口层加一层包装不要在业务代码里到处写权限判断。比如封装一个 AuthorizedDataService内部统一处理认证、授权、数据加解密对外只提供 getData、putData 两个方法。这样后续无论是换密钥策略还是改授权模型业务层都不用动。class AuthorizedDataService { final AuthStateMachine _auth; final AccessPolicy _policy; FutureDataBundle? getData(AtSign requester, String dataKey) async { final identity await _auth.ensureAuthenticated(requester); final token await _policy.issueAccessToken(identity, dataKey); return _dataStore.read(identity, dataKey, accessToken: token); } }4. 工程落地依赖替换、平台通道与安全存储选型4.1 让 Flutter 工程跑在鸿蒙上说句实话到了这一步网上资料已经很多我不展开太多。简单提一下必要动作使用支持 OpenHarmony 的 Flutter SDK 分支把项目里所有使用原生插件的地方都检查一遍为每个插件创建或适配 ohos 平台目录。at_commons 这种库的特点是它自己不直接依赖很多插件但它底层的网络和存储常常间接依赖。我的建议是先跑通一个最小 Demo再逐步接入完整业务。不要一上来就把整个项目拿过来编译否则报错信息混在一起你根本分不清是网络库的问题、存储库的问题还是密钥库的问题。我当时做了一个最小的验证清单按顺序打勾Flutter 工程能在鸿蒙设备上跑起来显示一个空白页面。能用 MethodChannel 从 Dart 调用鸿蒙原生方法并返回结果。能用 HUKS 在原生侧生成一对 RSA 密钥并用 sign/verify 做一次签名验证闭环。能把 flutter_secure_storage 的调用替换成自研的 SecureStore 通道。等这四步都通了再开始迁移 at_commons 的业务代码会顺很多。4.2 替换 flutter_secure_storage为什么不能直接继续用很多 Flutter 项目之前用 flutter_secure_storage 保存 atSign 私钥和持久化凭据。到鸿蒙上这个插件可能完全没有 ohos 实现也可能有社区版但建议不要直接用社区版的替代品因为存储安全级别很难验证。我采用的方案是自建一个轻量级 PlatformChannel 插件在原生侧使用鸿蒙系统的关键资产存储能力来保存密钥和凭据。Dart 侧保留统一的 SecureStore 接口API 设计得很简单write、read、delete。class SecureStore { static const MethodChannel _channel MethodChannel(at_commons_secure_store); Futurevoid write(String key, String value) async { await _channel.invokeMethod(write, {key: key, value: value}); } FutureString? read(String key) async { return await _channel.invokeMethodString(read, {key: key}); } Futurevoid delete(String key) async { await _channel.invokeMethod(delete, {key: key}); } }原生侧ohos 目录下的插件注册入口需要绑定对应的 MethodChannel并调用系统 API 完成加解密。这里的关键点是不要自己用普通文件存储凭据哪怕你做了 Base64 编码也等于裸奔。尤其是在鸿蒙这种多任务、多进程环境下文件权限控制一旦有疏漏凭据就可能会被其他应用读取到。4.3 网络层TLS、Socket 与自定义证书校验at_commons 的分布式通信底层需要访问多个节点的 HTTPS 服务还要维护长连接。鸿蒙的网络栈支持标准的 TLS但自定义 CA 证书的处理跟 Flutter 默认的 http 栈不完全一样。我遇到的实际问题是开发环境里用的内部证书不被系统信任导致 TLS 握手失败。排查之后发现需要把内部 CA 证书注入到网络请求的信任区域内。鸿蒙侧可以通过网络安全配置或系统能力来管理而不是在 Dart 层直接跳过校验。零信任模型下的原则是可以加信任但不能不校验。网络通道这块我还建议在适配时把连接参数例如握手超时、心跳间隔、重连策略集中到一个配置类里方便针对鸿蒙的实际网络表现做调优。分布式通信比普通 HTTP 请求更容易受到网络切换、进程回收、系统省电策略的影响没有集中的配置管理后面出了问题会很痛苦。4.4 插件工程结构参考把适配后的工程结构列一下方便对照at_commons/lib/src/secure_store/Dart 侧调用接口crypto_service/Dart 侧加密调度auth/认证状态机ohos/entry/src/main/ets/SecureStorePlugin.etsHUKSHelper.etsNetworkSecurityConfig.etsandroid/...保持原有实现这样的好处是 Dart 层公共代码完全跨端复用Android 和 ohos 只是平台实现不同。后面如果有人要维护或者做二次开发也清晰很多。每个原生模块只做一件事不要搞成一个大杂烩。5. 实测中追出来的问题轮转竞态、通道超时与异常恢复5.1 测试环境我的测试环境是一台 HarmonyOS 开发版设备和一台 Linux 服务器服务器上部署了 at_commons 对应的服务端节点。压测脚本用 Dart 编写主要模拟多客户端并发读写、密钥轮转、异常断开三种场景。5.2 问题一轮转过程中出现旧密钥与新写入的竞态这个问题的表现很怪轮转任务跑了不到一半新的数据写入回来了有些返回成功有些返回“数据解密失败”。追到原因之后发现是重新封装旧数据密钥还没有跑完新写入的数据已经用了新公钥封装但读取时还是按旧的索引去查找数据信封结果找到的是旧公钥封印的信封一解就失败。修复方法在数据信封的 envelope 结构里加一个 keyVersion 字段。读取时首先按 keyVersion 查找对应的密钥不存在则向上查找上一版本密钥写入时如果检测到旧版本密钥已经退役就先等待当前轮转任务完成再落盘。这个逻辑本质上是牺牲了一点点并发性能换取读写一致性。CipherEnvelope envelope await dataStore.readEnvelope(key); KeyPairInfo? key await keyService.lookupByVersion(envelope.keyVersion); if (key null) { key await keyService.lookupLatest(); } Uint8List plain await cryptoService.decrypt(key.id, envelope.cipher);5.3 问题二平台通道在密钥生成时的超时与卡顿第一次把 HUKS 的密钥生成串到认证流程里立刻发现一个体验问题用户点击登录后界面要空白 2 到 3 秒偶尔还超时。原因是 HUKS 生成 RSA 2048 密钥和签名操作比较耗时而 MethodChannel 默认是同步等待的一次签名卡住整个 UI 就跟着卡。修复方法把耗时的原生调用全部放进异步队列并降低一次通道调用的粒度。例如先生成密钥对再单独签名而不是一个方法里干完所有事。同时在 Dart 侧为认证流程增加进度提示把“耗时不可感知”变成“进度可见”用户体感反而更好。5.4 问题三长连接断开后的凭据恢复策略分布式通信最容易出现的就是节点重启、网络切换长连接突然断开。原库的默认逻辑比较依赖系统网络状态自动恢复。到了鸿蒙上我发现断线后的自动重连没问题但重连后如果本地的会话密钥已失效需要重新走挑战-响应认证。而 re-auth 必须在 3 次内成功否则会把本地临时凭据清空导致用户需要重新输入 atSign 和口令。这个设计本身是安全的但对普通用户的场景不够友好。我的处理是在临时凭据被清空之前增加了一个本地兜底把最近一次验证过的公钥指纹与当前密钥库中的指纹做比对如果一致则允许用指纹校验代替全量重新认证。这会降低一点点安全强度但换来了断线恢复的稳定。是否开启我做成一个配置项默认关闭由业务方自己决定。5.5 实测数据汇总整理一组有代表性的数据场景Android 基线鸿蒙适配后说明首次认证全链路850 ms1100 ms多了一次 HUKS 调用密钥轮转1000 个数据项2.8 s3.6 s主要耗时在重新封装平台通道单向调用5 ms4 ms差异不大TLS 握手可接受可接受使用系统网络栈断线重连reAuth400 ms620 ms状态机重建开销偏大总体上看鸿蒙适配后的性能没有出现不可接受的劣化首次认证和密钥轮转的耗时增加主要来自安全能力接口调用的差异。如果后续 HUKS 的调用方式再优化一下还有几个百分点的提升空间。适配过程中我个人感受最深的一点是在零信任架构下沉到鸿蒙、或者做其他平台适配的时候最重要的不是让所有代码都跨端复用而是识别出哪些安全语义是平台特定的然后抽象成边界。at_commons 这样的库尤其明显它的安全模型是核心资产平台实现要服务于安全模型而不是反过来。如果只是硬着头皮把代码编译通过表面省了事埋下的隐患会在你完全想不到的地方炸出来。
