基于signed_json 0.0.2版本完成OpenHarmony纯Dart适配,无需新增原生ArkTS代码,依托package:jose实现JWS验签、JWE解密能力,打通六大测试场景,实现Flu
开发工具: 华为云码道
本文配套仓库: icapps/flutter-signed-json
鸿蒙适配后仓库:https://atomgit.com/oh-flutter/flutter-signed-json
signed_json 用于在 Dart 侧校验并解密带签名 / 加密的远程配置 JSON:通过 JWS(ES512)做验签,通过 JWE(RSA1_5 + A128CBC-HS256)做解密,并支持先验签后解密的链式组合。本文以 signed_json 0.0.2 为例,介绍源码准备、OHOS 工程配置、example 演示与真机运行。
插件本身只在 Android 上有原生实现,方法通道 signed_json 也仅在 Platform.isAndroid == true 时被调用;OHOS 上没有新增任何 ArkTS 代码,平台走的是纯 Dart 分支(package:jose + compute()),因此不需要在仓库根目录创建 ohos/ 目录。key_hash 将鸿蒙的应用签名证书指纹读取能力封装为 Flutter 插件,应用可以通过 KeyHash.getKeyHash 获取当前应用的签名指纹。本文以 2a1f1962962f8fef5b128edcf9a52ed6a0c4824f 为例,介绍源码准备、OHOS 工程配置、ArkTS 实现和 example 真机运行。

插件目前支持 ohos 平台,上游源码位于 GitHub 仓库。文中的代码以上游 main 分支为适配基线,OHOS 改动记录配套仓库https://atomgit.com/oh-flutter/flutter-signed-json已经包含了 example/ohos/ 工程。文中的代码以提交 4230677eac3908ae40e70d61500df6f03447dadd 为参考,建议的 OHOS 适配 tag 为 0.0.2-ohos-1.0.0-beta.1。

| 序号 | 测试项 | 结果表现 | 耗时 | 状态 |
|---|---|---|---|---|
| 1 | verify:用 verify.jwk 公钥验签 remote_config.signed | 验签通过,返回 {“some_text”:“Something Something”} | 3312 ms | 通过 |
| 2 | decrypt:用 decrypt.jwk 私钥解密 remote_config.enc | 解密成功,返回 {“some_text”:“Something Something”} | 692 ms | 通过 |
| 3 | verifyAndDecrypt:先验签再解密的完整链路 | 完整链路成功,返回 {“some_text”:“Something Something”} | 167 ms | 通过 |
| 4 | verify(篡改签名):签名最后一个字符被翻转,应验签失败 | 按预期抛出异常:JoseException: Could not decrypt/verify payload | 106 ms | 按预期失败 |
| 5 | verify(错误证书):用不相关的 EC 公钥,应验签失败 | 按预期抛出异常:JoseException: Could not decrypt/verify payload | 3 ms | 按预期失败 |
| 6 | decrypt(错误证书):用不相关的 RSA 私钥,应解密失败 | 按预期抛出异常:JoseException: Could not decrypt/verify payload | 3 ms | 按预期失败 |
以下是操作的视屏,可以参考一下:
一、插件简介与适配目标
signed_json 把 JOSE(JWS + JWE)的能力封装为 Flutter 插件,业务层只关心“签名是否合法”与“能否解密出原文 JSON”,不需要自己引入 JOSE 实现。常见的使用方式是读取服务端下发的远端配置:服务端先用私钥签一份 JSON,再用公钥加密;客户端拿到 signed、enc 两段密文后,本地公钥验签 + 私钥解密,最终得到可消费的 Map<String, dynamic>。
调用模型是“一次调用返回结果”:Dart 侧发起 verify / decrypt / verifyAndDecrypt,原生侧读取自身签名材料或直接执行 JOSE 操作后,同步把 UTF-8 字符串(原始 JSON)返回给 Dart,再由 parseAndDecode<T> 解析成业务对象。不涉及持续监听或事件推送。
OHOS 适配目标有三个:
- 无需新增 ArkTS 原生代码:本库只在 Android 上有原生实现,方法通道
signed_json由lib/src/base_signed_json.dart的Platform.isAndroid分支控制;OHOS 上useNativeSignedJson为false,自动走纯 Dart 分支。 - example 应用跑通六个场景:包括正向的
verify/decrypt/verifyAndDecrypt以及三个预期失败的负向用例。 - example 工程结构正确:在
example/ohos/而不是在仓库根目录创建 OHOS 脚手架;pubspec.yaml的flutter.plugin.platforms不需要新增ohos:条目。
二、环境准备
环境搭建参考社区文档:Flutter OH 开发环境搭建,完成 Flutter OH SDK 安装、环境变量和 DevEco Studio 配置。
完成后,在宿主机终端执行以下命令,确认当前选中的是支持 OHOS 的 Flutter 工具链,并能发现目标设备:
flutter --version
flutter doctor -v
hdc list targets


工程使用的工具链和 SDK 配置如下:
| 项目 | 版本或配置 | 用途 |
|---|---|---|
| Flutter OHOS SDK | 3.44.9+ohos-0.0.1-canary1 | Flutter 编译与 OHOS 平台工具链 |
| Flutter 分支 | 0.0.2-ohos-1.0.0 | CPF-Flutter 对应开发分支 |
| Dart SDK | 3.12.2 | Dart 语言与包管理环境 |
| HarmonyOS 开发套件 | 7.0.0(API 26) | 开发套件版本及对应的 API 级别 |
compileSdkVersion | 工程未显式声明 | 由 DevEco 工程默认值决定 |
targetSdkVersion | 工程未显式声明 | 由 DevEco 工程默认值决定 |
compatibleSdkVersion | 5.1.0(18) | 当前工程声明的最低兼容版本 |
| 插件版本 | 0.0.2 | pubspec.yaml 中的包版本 |
| 原生语言 | ArkTS | 仅 example 模板使用,插件本身没有 OHOS 原生实现 |
| 插件产物 | HAR | 不适用:本库不在仓库根目录生成 ohos/ |
2.1 开发套件版本与工程中的 SDK 版本配置
7.0.0(API 26) 和 5.1.0(18) 涉及 HarmonyOS 开发套件版本(API 版本)及其底座 OpenHarmony 的版本号体系。在本文工程中,相关版本的含义与配置方式如下:
7.0.0(API 26)表示本机 HarmonyOS 开发套件版本为7.0.0,对应 API 26。5.1.0(18)是本文工程中compatibleSdkVersion的属性值,声明最低兼容 API 18。compileSdkVersion和targetSdkVersion未在本例的example/ohos/build-profile.json5中显式声明,编译时使用 DevEco 工程的默认值。
对应的 product 配置为:
{
"name": "default",
"compatibleSdkVersion": "5.1.0(18)",
"runtimeOS": "HarmonyOS"
}

本机使用 API 26 的开发套件编译,工程声明最低兼容 API 18。安装后能否正确解密 JWE、校验 JWS,取决于 JWE 加密时使用的 RSA / AES 密钥与客户端持有的私钥 / 公钥是否匹配,与设备 API 级别无关。
三、从源码仓库开始准备适配工程
3.1 将上游源码同步到自己的工作仓库
适配已有第三方插件时,先从 Pub 包信息或项目 README 确认上游地址、包名、版本和许可证。同步仓库时保留源码、许可证和提交历史,方便后续跟踪上游更新。
在 AtomGit 网页的新建仓库流程中,找到从已有仓库导入的入口,填写上游 Git 地址,选择自己有写权限的组织或个人空间,设置目标仓库名称后执行导入。完成后检查目标分支、pubspec.yaml、LICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在 AtomGit,也可以通过 Fork 获得自己的工作仓库。
signed_json 的上游仓库位于 GitHub,目前还没有 AtomGit 配套仓库。可以按上面的流程把 GitHub 上游导入 AtomGit 作为自己的工作仓库,也可以直接在 GitHub 上 Fork。下面使用 GitHub 上游地址拉取代码;需要提交修改时,使用自己有写权限的仓库或 Fork。
3.2 将代码拉取到宿主机
在安装了 Flutter OH 和 DevEco Studio 的开发电脑上打开终端,进入准备存放项目的目录,执行:
git clone https://github.com/icapps/flutter-signed-json.git
cd flutter-signed-json
pwd
ls
git remote -v
git status --short --branch
git rev-parse HEAD

git clone 会创建 flutter-signed-json/ 目录;cd 后的位置就是下文所说的插件仓库根目录,这里应能看到 pubspec.yaml、lib/、example/ 和 config/。Git 仓库名是 flutter-signed-json,Dart 包名是 signed_json。
本文的适配以 OHOS 适配前的上游提交为起点。需要使用与本文相同的代码版本时,在没有未提交修改的仓库中切换到以下提交:
git switch --detach 2a1f1962962f8fef5b128edcf9a52ed6a0c4824f
适配其他插件时,使用该插件对应版本的 tag 或 commit 作为分支起点。
路径提示:本文实际仓库位于
/Users/david/workspace/flutter/lib/flutter-signed-json(即~/workspace/flutter/lib/之下),不是~/workspace/flutter/flutter-signed-json。这是该 workspace 下多个 Flutter 插件共用一个父目录的常见路径陷阱,克隆或拉取时需要按实际位置进入。

图 1:拉取 icapps/flutter-signed-json 并检查远程与当前提交。
3.3 在仓库根目录创建适配分支
接着在 flutter-signed-json/ 根目录创建分支。分支名统一使用 feat/ohos_库名称_版本号,库名称取 pubspec.yaml 的 name,版本号取此次适配的基线版本。本例为:
git switch -c feat/ohos_signed_json_0.0.2
git branch --show-current
如果该分支已存在,使用 git switch feat/ohos_signed_json_0.0.2 切换即可。
建议在分支上补充 TAG,便于业务方按 tag 引用。建议的 tag 形如 0.0.2-ohos-1.0.0-beta.1(上游版本号 + -ohos- + 适配版本号 + 阶段标识)。上游仓库当前没有任何 OHOS 相关 tag,tag 需要在合并 / 发布前自行创建。

图 2:在 master 创建适配分支,并建议补充 TAG 0.0.2-ohos-1.0.0。
3.4 在 example 中补全 OHOS 适配结构
signed_json 与常见的带原生代码的 Flutter 插件不同:插件本身没有 OHOS 原生实现。pubspec.yaml 的 flutter.plugin.platforms 只声明了 android / ios / web 三端,方法通道 signed_json 只在 Platform.isAndroid == true 时被调用;OHOS 走的是 package:jose + compute() 的纯 Dart 分支。因此:
- 不需要在仓库根目录创建
ohos/目录。执行flutter create --template=plugin --platforms=ohos .会强行生成空的 HAR 脚手架,既不会带来任何 Dart 侧收益,也容易让下游业务方误以为有原生实现要对接。 - 只需要在
example/中补全 OHOS 应用工程,让示例 App 能在 OHOS 上跑起来。
进入 example/ 后执行:
cd example
flutter create --platforms=ohos .
cd ..
--platforms=ohos指定需要补全的平台。- 最后的
.表示在当前example/目录补全工程,不会另建一层目录。
补全后会生成 example/ohos/(包含 AppScope/、entry/、build-profile.json5 等)。生成后通过 git status 检查新增文件,保留示例原有 lib/main.dart 等,必要时再覆盖 demo。不同 Flutter OH 版本生成的模板可能略有差异。

图 3:在 example/ 目录执行 flutter create --platforms=ohos . 后新增的 example/ohos/ 目录。
3.5 适配后的项目目录
适配后的关键目录如下:
flutter-signed-json/
├── lib/
│ ├── signed_json.dart
│ ├── signed_json_web.dart
│ └── src/
│ ├── base_signed_json.dart
│ ├── signed_json.dart
│ ├── bridge/native_signed_json.dart
│ └── model/computer_args.dart
├── android/ # 原生 Kotlin 实现
├── ios/ # 原生 Swift 实现
├── web/ # Web 端实现
├── example/
│ ├── lib/main.dart # 演示 6 个场景
│ ├── tool/ # 调试脚本(ohos_chain_test.dart 等)
│ └── ohos/ # OHOS 应用工程(无原生插件)
│ ├── AppScope/
│ ├── entry/
│ └── build-profile.json5
├── config/
│ ├── keys/ # verify.jwk / sign.jwk / decrypt.jwk / encrypt.jwk
│ ├── remote_config.json
│ ├── remote_config.signed # JWS(ES512)
│ └── remote_config.enc # JWE(RSA1_5 + A128CBC-HS256)
├── docs/
│ └── ohos-test-evidence/ # 真机运行证据
├── test/
└── pubspec.yaml
项目根目录如下,其中包含 lib/、example/ohos/、config/ 和 docs/ohos-test-evidence/:

图 4:适配后的 flutter-signed-json 项目目录,包含 lib/、example/ohos/、config/、example/tool/ 与 docs/ohos-test-evidence/。
| 文件 | 主要职责 |
|---|---|
lib/signed_json.dart | 入口文件,转发到 lib/src/signed_json.dart |
lib/src/signed_json.dart | SignedJson 类,公开 verify<T> / decrypt<T> / verifyAndDecrypt<T> |
lib/src/base_signed_json.dart | SignedJsonUtil,按 Platform.isAndroid 决定走原生还是纯 Dart |
lib/src/bridge/native_signed_json.dart | Android 原生 MethodChannel signed_json 的 Dart 封装 |
lib/src/model/computer_args.dart | compute() 跨 isolate 传递的参数模型 |
android/ | 原生 Kotlin 实现(com.icapps.signedjson.SignedJsonPlugin) |
ios/ | 原生 Swift 实现 |
web/ | Web 端实现 |
config/keys/ | 仓库自带的 JWK 密钥对与样例配置 |
example/lib/main.dart | 6 个场景的演示(含耗时显示) |
example/tool/ohos_chain_test.dart | 宿主端验证 + 生成 chainEncoded |
example/ohos/ | OHOS 应用工程,无原生插件代码 |
四、Dart 接口与通道分析
OHOS 实现需要遵循 Dart 层已有的方法、参数和返回值约定。先阅读 lib/signed_json.dart、lib/src/signed_json.dart 和 lib/src/base_signed_json.dart,确认平台分支逻辑;本库在 OHOS 上不需要新增任何 ArkTS 代码,原因是方法通道 signed_json 只在 Android 上有效,OHOS 自动走纯 Dart 分支。适配其他已有平台的插件时,也应保留其公开接口和其他平台实现。
本例的对应关系如下:
| Dart 入口或模型 | 通道协议 | OHOS 实现 | 应保持的行为 |
|---|---|---|---|
SignedJson.verify<T> | signed_json / verify(仅 Android) | OHOS 不走通道,调用 _verifyOnBackgroundThread | T 为泛型,parseAndDecode<T> 解析;useNativeSignedJson == false |
SignedJson.decrypt<T> | signed_json / decrypt(仅 Android) | OHOS 不走通道,调用 _decryptOnBackgroundThread | T 为泛型;decryptionCert == null 时抛 ArgumentError |
SignedJson.verifyAndDecrypt<T> | signed_json / verifyAndDecrypt(仅 Android) | OHOS 走 internalVerify + internalDecrypt 串联 | 先 JWS 验签、解出 JWE 字符串,再 JWE 解密出原文 JSON |
parseAndDecode<T> | — | compute() 中的类型强转 | 调用方必须显式传 <Map<String, dynamic>>,否则会出现类型转换异常 |
原生端需要保持通道名、方法名和参数一致(仅 Android)。系统能力不可用或密钥不匹配时,由 package:jose 抛出 JoseException。
4.1 跨端架构与调用时序
Flutter 侧和 HarmonyOS 侧之间不需要任何通道:OHOS 走纯 Dart 分支,SignedJsonUtil.useNativeSignedJson 在 OHOS 上返回 false,整个 JWS / JWE 处理都通过 package:jose 在后台 isolate(compute())完成。
OHOS 走右侧分支:compute() 把 ComputerArgs 跨 isolate 传到 _verifyOnBackgroundThread / _decryptOnBackgroundThread,由 package:jose 完成 JWS / JWE 处理,返回 UTF-8 字符串后再用 parseAndDecode<T> 解析成业务对象。
4.1.1 一次 verifyAndDecrypt 调用的时序
链式调用是先验签、解出 JWE 字符串,再解密出原始 JSON。两步都通过 compute() 走后台 isolate,避免主 isolate 阻塞 UI。
4.2 公开 API 与平台接口
lib/src/signed_json.dart 中的 SignedJson 类是业务层唯一入口:
class SignedJson {
final _signedJsonUtil = SignedJsonUtil();
final String verificationCert;
final String? decryptionCert;
SignedJson(this.verificationCert, {this.decryptionCert});
Future<T> verify<T>(String encoded) async => _signedJsonUtil.run(
parseAndDecode,
await _signedJsonUtil.internalVerify(verificationCert, encoded));
Future<T> decrypt<T>(String encoded) async {
final decryptionCert = this.decryptionCert;
if (decryptionCert == null) {
throw ArgumentError('Decryption key can not be null');
}
return _signedJsonUtil.run(parseAndDecode,
await _signedJsonUtil.internalDecrypt(decryptionCert, encoded));
}
Future<T> verifyAndDecrypt<T>(String encoded) async {
final decryptionCert = this.decryptionCert;
if (decryptionCert == null) {
throw ArgumentError('Decryption key can not be null');
}
return _signedJsonUtil.verifyAndDecrypt<T>(
verificationCert, decryptionCert, encoded);
}
}
调用方式和使用时机如下:
verifyCert/decryptionCert在构造时一次性传入,建议使用config/keys/下的真实 JWK;- 三个方法都是
Future<T>形式,T由调用方指定;上游示例默认使用Map<String, dynamic>; decrypt/verifyAndDecrypt在没有decryptionCert时直接抛ArgumentError,不走原生通道;- OHOS 上三者的实现都走
_signedJsonUtil.internalVerify/internalDecrypt,并在compute()后台跑package:jose。
4.3 平台分支:lib/src/base_signed_json.dart
SignedJsonUtil 是 OHOS 与 Android 行为分叉的核心:
class SignedJsonUtil {
late final Future<void> computerStarting;
bool get useNativeSignedJson => Platform.isAndroid;
Future<R> run<P, R>(FutureOr<R> Function(P) function, P param) async =>
compute(function, param);
Future<String> internalVerify(String cert, String encoded) async {
if (useNativeSignedJson) return NativeSignedJson.verify(cert, encoded);
final map = ComputerArgs(cert: cert, encoded: encoded).toJson();
return run(_verifyOnBackgroundThread, map);
}
Future<String> internalDecrypt(String cert, String ciphertext) async {
if (useNativeSignedJson) return NativeSignedJson.decrypt(cert, ciphertext);
final map = ComputerArgs(cert: cert, encoded: ciphertext).toJson();
return run(_decryptOnBackgroundThread, map);
}
Future<T> verifyAndDecrypt<T>(
String certVerify, String certDecrypt, String encoded) async {
String result;
if (useNativeSignedJson) {
result = await NativeSignedJson.verifyAndDecrypt(
certVerify, certDecrypt, encoded);
} else {
result = await internalDecrypt(
certDecrypt, await internalVerify(certVerify, encoded));
}
return run(parseAndDecode, result);
}
}
useNativeSignedJson 直接读取 Platform.isAndroid。OHOS 既不是 Android 也不是 iOS,因此 useNativeSignedJson 始终为 false,三个方法都走纯 Dart 分支:internalVerify → compute(_verifyOnBackgroundThread) → package:jose 验签并 ZLibCodec().decoder 解压;internalDecrypt 同理;verifyAndDecrypt 在 Dart 侧串联两步。
后台 isolate 收到 ComputerArgs 后,调用 package:jose 的 JsonWebSignature.fromCompactSerialization / JsonWebEncryption.fromCompactSerialization,再用 JsonWebKeyStore + JsonWebKey.fromJson 注入密钥,最后把 payload 的 zlib 字节流解压为 UTF-8 字符串返回。
4.4 Dart 通道协议分析
4.4.1 通道名称仅在 Android 上有效
static const MethodChannel _channel = MethodChannel('signed_json');
通道名 signed_json 是 Android 原生侧约定的。Dart 侧调用仅在 Platform.isAndroid == true 时发生,OHOS 上不会触发 invokeMethod,因此不存在“OHOS 通道拼写不一致导致收不到结果”这类问题。如果上游未来在 OHOS 上也走原生通道,需保证两端字符串完全一致。
4.4.2 仅在 Android 触发的原生调用
static Future<String> verify(String cert, String encoded) async {
final zipBytes = await _channel.invokeMethod<List<int>>(
'verify',
{
'cert': "{\"keys\": [$cert]}",
'encoded': encoded,
},
) ??
[];
final unzipped = ZLibCodec().decoder.convert(zipBytes);
return utf8.decode(unzipped);
}
原生侧约定:参数 cert 会被外层包成 {"keys": [...]} 的 JWK Set;encoded 是 JWS / JWE 紧凑序列化字符串;返回值为 List<int>,是 zlib 压缩后的字节流,Dart 侧用 ZLibCodec().decoder.convert 解压并 utf8.decode 还原。OHOS 不调用这一段,因此无需关心原生侧的实现细节。
4.4.3 调用结束与资源回收
三个公开方法都是一次性调用:没有 EventChannel、没有订阅,也没有需要取消的监听。每次调用相互独立,返回值到达后本次调用即结束。后台 isolate 由 compute() 在返回后自动回收,业务层不需要额外做清理。
五、补全 OHOS 工程配置(不新增原生代码)
5.1 本库没有 OHOS 原生实现
signed_json 的平台差异完全集中在 Dart 侧 lib/src/base_signed_json.dart 的 Platform.isAndroid 判断上。OHOS 上 useNativeSignedJson 为 false,方法通道 signed_json 不会触发,因此:
- 不需要在仓库根目录创建
ohos/目录,不需要写SignedJsonPlugin.ets之类的 ArkTS 插件; pubspec.yaml的flutter.plugin.platforms不需要新增ohos:条目;GeneratedPluginRegistrant.ets不会注册任何插件类(保持空即可)。
适配本库时,所有 OHOS 相关工作都集中在 example/ohos/:补全 OHOS 工程脚手架、修正示例应用配置、整理 demo。下面只介绍 OHOS 工程配置相关的差异点。
5.2 声明插件和宿主权限
signed_json 的 JOSE 实现是纯 Dart(package:jose + ZLibCodec),不读取传感器、不访问用户存储、不访问网络,因此插件 HAR 和宿主 entry 都不需要任何敏感权限。
5.2.1 不需要插件 HAR
本库没有生成 ohos/ HAR,示例工程直接引用 Dart 包,因此没有插件 HAR 的 module.json5 需要维护。
5.2.2 应用 entry 的权限
示例应用的 example/ohos/entry/src/main/module.json5 只保留了 Flutter 模板自带的 ohos.permission.INTERNET,它与 JWS / JWE 处理无关:
{
"module": {
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" }
]
}
}
signed_json 既不读取签名材料,也不读取设备传感器,因此不需要 DETECT_GESTURE 之类的敏感权限,接入应用时也不会出现运行时授权弹窗。INTERNET 属于普通权限,无需额外填写权限原因资源。
5.3 修正 example 应用配置
5.3.1 修正 AppScope 的 bundleName(重要坑)
example/ohos/AppScope/app.json5 中初始生成的 bundleName 是 com.example.keyboardheightpluginexample,这是 flutter create 从另一个插件模板(keyboardheightpluginexample)拷贝过来的残留值。如果直接使用,会导致两个问题:
- 应用包名与示例工程名(
signed_json_example)不一致,DevEco 工程的资源定位可能错位; - 与设备上已安装的
keyboardheightpluginexampleDemo 冲突,签名 / Profile 也会跟着错。
应改为与示例工程一致的 bundleName:
{
"app": {
"bundleName": "com.example.signed_json_example",
"vendor": "example",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:app_icon",
"label": "$string:app_name"
}
}
修改后,DevEco 工程的 entry 模块识别、签名、Profile 选择都会以新的 bundleName 为准。提交前需要确认仓库其它位置没有再硬编码 keyboardheightpluginexample。
5.3.2 清理 example/ohos/build-profile.json5 的签名材料
flutter create 与 DevEco Studio 会自动往 example/ohos/build-profile.json5 写入本机调试签名材料,本例当前文件包含:
signingConfigs[*].material.certpath、storeFile、profile:本机绝对路径;keyPassword/storePassword:调试签名口令(Base64 字符串)。
这些内容必须从待提交版本中移除,再替换成不含敏感信息的通用配置:
{
"app": {
"signingConfigs": [
{ "name": "default", "type": "HarmonyOS" }
],
"products": [
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "5.1.0(18)",
"runtimeOS": "HarmonyOS"
}
],
"buildModeSet": [
{ "name": "debug" },
{ "name": "profile" },
{ "name": "release" }
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{ "name": "default", "applyToProducts": ["default"] }
]
}
]
}
提交前用 git diff -- example/ohos/build-profile.json5 复核,确保不包含绝对路径或口令。签名材料保存在本机,公开仓库中只保留构建所需的通用配置。
5.4 注册插件:无原生插件类可注册
因为 pubspec.yaml 没有声明 ohos: 平台,example/ohos/entry/src/main/ets/plugins/GeneratedPluginRegistrant.ets 不会被注入任何插件注册代码。当前生成的 registerWith 函数体为空:
static registerWith(flutterEngine: FlutterEngine) {
try {
} catch (e) {
Log.e(TAG, "Tried to register plugins with FlutterEngine (" + flutterEngine + ") failed.");
Log.e(TAG, "Received exception while registering", e);
}
}
这是预期行为,不要在 registerWith 中手工添加 signed_json 的注册代码——本库没有 OHOS 原生插件类。示例应用启动时,Dart 侧会按 Platform.isAndroid 自动走纯 Dart 分支。
5.5 检查 example 的 OHOS 应用结构
本例的 example/ohos/build-profile.json5 应在 products 中设置版本。下面是需核对的配置片段,请合并到现有工程;其中 signingConfig: "default" 需要与本机配置的签名名称一致,签名材料保留在本地:
{
"app": {
"products": [
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "5.1.0(18)",
"runtimeOS": "HarmonyOS"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": ["default"]
}
]
}
]
}
本例的 product 没有显式声明 compileSdkVersion 和 targetSdkVersion,编译时使用 DevEco 工程的默认值。配置后,在 DevEco Studio 中执行一次 Sync Project。如果 entry 模块正常识别,Project 视图中会出现 entry,并能打开 entry 模块的签名配置。
六、补全交付文件并提交适配分支
6.1 除代码外还要补全哪些文件
代码适配完成后,还需要整理安装说明、接口文档、版本记录和开源信息。接收仓库有专用模板时,按其格式填写:
| 文件 | 应写清楚的内容 |
|---|---|
README.OpenSource | 上游名称、源码地址、适配版本或提交、版权及许可证信息;按仓库模板列出第三方依赖 |
README.md | 原项目说明、OHOS 支持入口、配套 Demo 和文档链接;保留上游信息 |
README.OpenHarmony_CN.md | 简介、AtomGit 安装方式、版本对应关系、环境约束、权限、接口表、示例、已验证范围和遗留问题 |
README.OpenHarmony.md | 与中文说明对应的英文文档 |
CHANGELOG.OpenHarmony.md | OHOS 新增能力、适配版本、兼容限制与测试范围 |
LICENSE / NOTICE | 保留上游许可证;NOTICE 按许可证和原项目要求保留或补充 |
example/README.md | 依赖方式、运行目录、签名、操作步骤与效果图;覆盖六个场景 |
pubspec.yaml、example/ohos/build-profile.json5 | 核对包名、版本、仓库地址、许可证和签名占位 |
.gitignore | 忽略构建缓存及本机签名材料,不漏提交必要源码和配置 |
signed_json 的上游包名为 signed_json,版本为 0.0.2,采用 MIT 许可证;Flutter 和 HarmonyOS SDK 版本写入环境说明。当前仓库没有 README.OpenHarmony_CN.md、README.OpenHarmony.md 和 CHANGELOG.OpenHarmony.md,OHOS 适配 PR 中应按模板补全。
6.2 提交前检查
提交前先完成第八节的插件、example 和真机测试,再从根目录检查改动:
git branch --show-current
git diff --check
git status --short
git diff --stat
git diff
重点检查:
example/ohos/AppScope/app.json5的bundleName已经改成com.example.signed_json_example;example/ohos/build-profile.json5不含绝对路径与口令;example/ohos/entry/src/main/module.json5只保留ohos.permission.INTERNET;docs/ohos-test-evidence/下保留关键证据(见第八节);pubspec.yaml的flutter.plugin.platforms仍然是android/ios/web,没有新增ohos:条目。
6.3 提交并推送
文档和代码整理完成后,在根目录暂存并提交。文件名按项目实际情况调整:
git add lib config example/pubspec.yaml example/lib example/ohos example/tool docs
git add README.md README.OpenSource README.OpenHarmony_CN.md
git add README.OpenHarmony.md CHANGELOG.OpenHarmony.md
git diff --cached --check
git diff --cached --stat
git diff --cached
git commit -m "feat: add OHOS support for signed_json 0.0.2 (pure-Dart path)"
git remote -v
git branch --show-current
git push -u origin feat/ohos_signed_json_0.0.2
DevEco 可能向 example/ohos/build-profile.json5 写入本机签名配置,需要在提交前从暂存内容中移除。推送时,origin 应指向自己导入或 Fork 的仓库(AtomGit 或 GitHub),当前分支为 feat/ohos_signed_json_0.0.2,并按需补充 tag 0.0.2-ohos-1.0.0-beta.1。
推送后在对应托管平台发起合并请求,说明上游来源和版本、OHOS 实现范围(无原生代码、纯 Dart 分支)、依赖及权限(仅 INTERNET)、测试环境、操作结果、已知限制(compute() 在 main isolate 之外、JWE 解密依赖 JWK 私钥),并附 Demo 运行图。目标分支和评审流程以接收仓库要求为准。
七、使用根目录 example 演示接入
仓库自带 example/,可以直接用来调试插件和体验 JWS / JWE 处理。
7.1 本地适配时使用路径依赖
当前 example/pubspec.yaml 的依赖是:
dependencies:
flutter:
sdk: flutter
signed_json:
path: ../
../ 相对于 example/pubspec.yaml 指向插件根目录,修改根目录插件后可直接联调。
7.2 通过 Git 仓库引入插件
业务应用通过 Git 引入时,将 signed_json 的 path 配置替换为下面的 Git 依赖。这里固定到本文使用的提交:
dependencies:
flutter:
sdk: flutter
signed_json:
git:
url: https://github.com/icapps/flutter-signed-json.git
ref: 2a1f1962962f8fef5b128edcf9a52ed6a0c4824f
使用自己的适配版本时,先推送分支,再将 url 改为对应仓库,ref 改为 feat/ohos_signed_json_0.0.2。正式发布后可固定到 tag 或 commit,例如 0.0.2-ohos-1.0.0-beta.1。
signed_json 还依赖 jose ^0.3.2 和 computer ^3.2.1。这两个包都是纯 Dart 库,不需要 OHOS 平台分支,Pub 会直接解析到上游版本。从插件根目录执行:
cd example
flutter pub get
flutter pub deps
检查 example/pubspec.lock 中 signed_json 的来源为 git,并核对 url、ref 和 resolved-ref。同时检查没有 dependency_overrides 或 pubspec_overrides.yaml 将其覆盖回本地依赖,确认应用使用的是 Git 依赖。
7.3 调用接口实现 6 场景演示
signed_json 的核心 API 是三个带泛型的方法。示例页面把它扩展成 6 个场景,3 个正向 + 3 个负向,分别覆盖验签、解密、链式组合以及篡改 / 错误证书 / 错误解密密钥的预期失败分支。下面这段可直接用于 example/lib/main.dart,对应仓库 example/ohos-test-evidence/evidence2_full_scenarios.jpeg 和 evidence3_final_red_icons.jpeg:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:signed_json/signed_json.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
static const verifyCert = '''{
"kty": "EC",
"use": "sig",
"crv": "P-521",
"kid": "signed_json",
"x": "AT0MMaUvyZdRZaCIqkLZPks0NE4kPZhgTwIivnr0tDhcl7Ao9SSdiXPuCsZS3HaAuQq5Kk1sSWGwNtteiq5JsiSg",
"y": "AVKxKaIYJewNWA3DvD5qzpW9vSMt6A1crUqVpQ-MKAOVsCL_RwwvQoedRoMbYpf6T7XLflECaI58pBtv8JfF8zVb",
"alg": "ES512"
}''';
static const decryptCert = '''{
"p": "2DXmKMTnT1t8mi7MWuDn9YJ9aymtRO-L-Q2Hj3porAXYyeifD6tWpd3B283SzBNmJMSzb9ahCRrIXJbrh8xPoQWAacY-zEgth_Eujl0r4KyzjzOwFwjhk_9dB2g0PZx1bsZtW5Crq8s_NAyURJP45bzS4yYYpI6PT7e63bfchZ0",
"kty": "RSA",
"q": "sag_C3sbeLOdhzDknfs8zMsy6uFfBjmAT2BrE3-K5QHZXOCQ7304VhRIvt3BeDUYDF1Onu4RYB7yRfcYkI1yG5GI-Zx2jTwcGhADIs-ovGsKbY7H5SDLjAGNWOj4NfW2ObuuVB68DCeaatHJ30XXilFw9gRZBLtsgxtK9BPPHnc",
"d": "hlv6XGfcUNJj5eTKCTDgYZuVrucW9Qd5fMIewRj-BSjuXycyGpqEsLQegv4p3Qan_FwQYHRshh6pDl8f746BsPspsQGvZwyht3y5HZfz_XJ0sY4gWNT5fAg6G3ch9W53PTEb2zX7SLKyWpM1H0Qapdt8r5WqC1oYNpHYc88mImk71pBLd_ywWvYqahq52VOyEiXHslRz4Wb8OFhrlU59aV7EErHMbuB6KdLpfTQBmjSEVEbeHRrzmGpgPfNXKRxBiN50DTfObBt3wDYxyO87F8fJm4pjqwwa034J_WROEa0YUPqjvn2Xj9KRRZhgoKmQUVmApjIr7oqqg84BSONGMQ",
"e": "AQAB",
"qi": "EYowTKshuJPVcmzUKuYmBYhAah0_AikBOO6lZLP65SGb3frVWBqAFGeKL-K72c4Cwlt74dZHVbkPVRUpFQuKGXtFI6DJTtorWdfvFUblsB2f-OeEK8QPSz5QGmSywZ8oD11hbpWUdua764ZqXQaEZnHyUUbUGguxEPMA8GHdQhk",
"dp": "hcv0_k27htRqq09CjwqXAMsbqfFElGBZEmpY9WUe2TVVDr2xkRTKriIpEUixpjBrCV3gXNlJFkVIsGOEpai9rjulV8-ilPAlnPaXhOoLeSHmjDvEQLzyO4_PlgHaMjZcRYztp7hDRDCmkCMorbeUUzcimga9QTgnX4GnVgWtpdE",
"dq": "dPccauamc5VuBW__VLPwl7TA1TuEYIjDHX-Rf8jdHWFWRnvjcIm06Zd5PZCqrAXoy1szRBfhgLNfNwk0NxepJNVwpUaKFvqYVeBs8CJgKY0f1HnIyeYJnSf4c60Onhgj3Wbfo6qIjEgWtnVgv4swGXT9Njwuj5sGGluBwai5GIk",
"n": "lgtcwDDTKxWLLnpVTGCad7LWnRPSO3nh0Imd1GT0cxZyIggwYDuaHPN6IQQc6Lv68_giQRZmWSjntQGAfvNdIXY38QstgD11P2ks3LtfQBdkBnfmIudCTRQr4eOHn4t_UErKRJB4LnmLTtlkqgbC0wj4lsSRWld8gj8thbwLoLKry8ouvwV2G_ST7SLecVQsaK5zceIV_AirXHnCgVHLC_RFcyWcYj1PlmwoSzvC4AcLj3GBPbV-ag_khXbaGp_fEt7niFhGtx2QtiWHv3UJuYFJ_mn81QW_NGJwa3qepKvc_F4HnVWCzkcpALJbjUsZKio1ETmvpLqPmJ17AB2B-w"
}''';
static const wrongVerifyCert = '''{
"kty": "EC", "crv": "P-521",
"x": "AaKWEFHhtQe2Deti4v8ZvVnatjAeHQg3T1DicxaHHQGTtZBUz2lHYRkoaxOUnbKeCiYjtvlZGk5yP_pRZ1h4_wXi",
"y": "AV1-NvlTWgTinqGb32Osey9ZL5B980kbkNj3hj8ZjkRy5cKLdc0D7TGlek4lMZ--mWCCAlrEJSP8NJx4lKLBdczR",
"alg": "ES512"
}''';
static const wrongDecryptCert = '''{
"kty": "RSA",
"n": "hUr7FueINW0xQkMop9jEnTaCBcqwFfI7QJ3hkFPNi4rVtQYJtXuCYGsmq9CgMS_DzKwcWg2TQ1W8k6VpgVWLZEu_8G6Ew8ZQHdTADGJfeUieIYn8vSjWtoXN8VS3GFArhNWtPWLus-exGQMvz02CqALLnoiyoGIZIC1--X0C2H7x5IsKPtw6CfddBqcspQ3X0g01BHPZgpyqW3SsKVGHIyUlKNFnUDhjoSu47PayxgK3UiRdN4kciGUYNkDEM4Zpzk6L8x9_72b1xXpzln8S4h9te_Q_5X8T4Auq2tgzF54WREPN9AdyWtmAHM7cIgtJPMc--GxyiDbF-3vTWSYumQ==",
"e": "AQAB",
"d": "JIcv2E8LHNkXrrkI4zacaxkM-Nla-Cix5DtgHVVZ9uvNNRa6gmmeiR3UMzGxNMmKNwTToDooKUPNsgiaqT7wPEQmDZW7_IrUWdh76OjskSg9baOLB6uxa8OvdHtq0dbmljiYiUIbeGH-PoSJDZ6IN9LMSl3b1egMSq0tJuIDbaC3GniYt_uTeNOg8atKm1jdnB3qqfULYcnb8_HU4ujxtm2YvCM5Gh90vESqA8gMlfnebqDduOclyuXUSVlvknBXxXu32ZjEFBHuIp54yngnjF63YYAZ9H6J7vrgEdysO1kCpbaJCVAKBujYK4C4FFzjDhDpC2HSTxY5-lrP1iQsIQ==",
"p": "uNm5c5yTgCHf2NZLFsU8QrZ4XRZFh01F_760GJcl3cRmjetKNfkBulLi3E-S2jwYBSB93k4NK7DnEqDZ2ffLg4O2i1lk8Xaz3PyxSi4LhJ3I-ysppfMkh4ZKmeC7WJxXPMPeZG5YR9MGNhHuELB-B6eM-m8hY4YrgrLuYvZVVg8=",
"q": "uJkFFcU6_kM3MRlD2ZI6XQwjDsf8EkDlH6O9Xym_uOMlALHIkmtGafclFJkHBOzNyIl24frcizX49YRUbcUjjHc-hqAbD18vW_90gIOw_WICsba-H2llLPEeeK2HulMQJKqSIVe232YlLT6ite7qUF1o602AAINZBFR1Hn9-mNc=",
"alg": "RSA-OAEP", "use": "key"
}''';
static const encoded =
'eyJhbGciOiJFUzUxMiIsImtpZCI6InNpZ25lZF9qc29uIn0.eNqr5lJQUCrOz02NL0mtKFGyUlAKBnJKMjLz0hXgLCWuWgALBg2D.AXMx-2iQVtaDIaV14lXhw7j6hW0D0HKyWYu9kUz1W4W0j8gOlDU2YWipyEDWGAGXKgdmIpWl1SeWrVlXdJeVLYFlAe2pqriYxevIzEErRdP1VwLDP7lCiLERCaiz_usAAY2fiHVNqEqFhPr8bxpnWIxiEcoG2BB5zUEoEH_kZ51I0jgV';
static const tamperedEncoded =
'eyJhbGciOiJFUzUxMiIsImtpZCI6InNpZ25lZF9qc29uIn0.eNqr5lJQUCrOz02NL0mtKFGyUlAKBnJKMjLz0hXgLCWuWgALBg2D.AXMx-2iQVtaDIaV14lXhw7j6hW0D0HKyWYu9kUz1W4W0j8gOlDU2YWipyEDWGAGXKgdmIpWl1SeWrVlXdJeVLYFlAe2pqriYxevIzEErRdP1VwLDP7lCiLERCaiz_usAAY2fiHVNqEqFhPr8bxpnWIxiEcoG2BB5zUEoEH_kZ51I0jgg';
static const encryptedEncoded =
'eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiUlNBMV81In0.a5a73YZgaQwTH3V5pqMXUIuAxKUqZdH5rQxN9RBWNiizJK03BamKqOZjbHuWv18qwAkgWxudRXF4io2vVZv5gChmpwQ3KjzS2z4ccMY5GNnf-_5SSX2bzIvoEJvBaFvseA049cBsdvcfTKKZZXJXe_z4k_iB-hyvo5JOQ8ki30sToDeMT9sVkLfh9icMP-DJ73zzqmsK9twpZgjChmEdHqJbx5xmejfSjdW76-RTWm3LEFkTBsg_Ye3Lupj9qmAkIQvC9Xgbvu2tCcmgSLpQz135H33GSP_cYnRfX0ho-nXPOpvba4ASJL68S_BBmrpCTG2U1w95RKufqVhwKKZUQg.tSsXy0GVk6CsJnnkbjO-nQ.KPFLPN8tLK3vqAvLvLO9IO6Q88vPVPNO1YoiP3tsaP_sbNTa92W514eakZ0QiaNU.5TWtnlE_Bbx4xo95mRoITw';
static const chainEncoded =
'eyJhbGciOiJFUzUxMiIsImtpZCI6InNpZ25lZF9qc29uIn0.eJwFwce2Q0AAANAvkoMxCUuk6W3UjYMoQ7SHIb7-3Vv-1G_eG9jCqmQghXNo9a77DTJan1XwjuPwGJR2xP7XlIyAZ5SBvmQwu4E4qTNnR28QwGk2Il_ZxEPz5-Tzhn_OYQquFJoYn6pGAynrtdlK2vy9hYTh513s6vDYPm705PDIkiAhsJabftodoLWnx55cURgxfJlDRaXQ8yI2PxUyPlQiZU-ylCLNCYW0fEhRIU1LkkiNyvTkuhRLVPMjI1Qth-8woBc03ksDCUvQ6VUj4MKwqbt6A-c594smrPuU1K3c9I_Pe1bzAx592VZe-wlvV8pFYQ_0x7ND0lKncQn0bWqFuRc7xSGyENU52dhVLvra0yfnZAB8A_Dy7LSIB7eK6Gakhsi2JpJnnOip-pX3Uknq_yYZvVif2QXoals1B82uaYnv1JfVW6If_Qq6q7yow9DlrUUNzkWzn7pt8quuATKLRCe6JSjW1eF5Yge2aTHxiG2wLpmdLrmJMoENIcOVWZfQDs5M_wJRuA7fRyrlB3eMAuzdUUH7PzOgqmA.AOfUPM3bUC784fb4IKnNhq5jNx6WvFm2TajV8GMx0x0Xv0ucHKagkNhwH1Gta-SrwcKjbw34KrY4hDRkTiBliiy2APEs4xH_gwJ0ifRYwBNXR6RABtN32UOV_pYaZ6Iftuw5eST4V0lNjYjR0ikzcPZy9UVxRWl2fLJFHLSrWcY6-ad7';
final _signedJson = SignedJson(verifyCert, decryptionCert: decryptCert);
final _wrongVerifyJson = SignedJson(wrongVerifyCert);
final _wrongDecryptJson =
SignedJson(verifyCert, decryptionCert: wrongDecryptCert);
final List<_ScenarioResult> _results = [];
var _running = false;
void initState() {
super.initState();
_runAll();
}
Future<void> _runAll() async {
setState(() {
_running = true;
_results.clear();
});
await _run('1. verify:用 verify.jwk 公钥验签 remote_config.signed',
() async => jsonEncode(
await _signedJson.verify<Map<String, dynamic>>(encoded)));
await _run('2. decrypt:用 decrypt.jwk 私钥解密 remote_config.enc',
() async => jsonEncode(
await _signedJson.decrypt<Map<String, dynamic>>(encryptedEncoded)));
await _run('3. verifyAndDecrypt:先验签再解密的完整链路',
() async => jsonEncode(await _signedJson
.verifyAndDecrypt<Map<String, dynamic>>(chainEncoded)));
await _run('4. verify(篡改签名):签名最后一个字符被翻转,应验签失败',
() async => jsonEncode(await _signedJson
.verify<Map<String, dynamic>>(tamperedEncoded)),
expectFailure: true);
await _run('5. verify(错误证书):用不相关的 EC 公钥,应验签失败',
() async => jsonEncode(
await _wrongVerifyJson.verify<Map<String, dynamic>>(encoded)),
expectFailure: true);
await _run('6. decrypt(错误证书):用不相关的 RSA 私钥,应解密失败',
() async => jsonEncode(await _wrongDecryptJson
.decrypt<Map<String, dynamic>>(encryptedEncoded)),
expectFailure: true);
if (mounted) setState(() => _running = false);
}
Future<void> _run(
String title, Future<String> Function() op,
{bool expectFailure = false}) async {
final sw = Stopwatch()..start();
_ScenarioResult r;
try {
final value = await op();
sw.stop();
r = _ScenarioResult(
title: title,
success: !expectFailure,
errorPath: expectFailure,
detail: expectFailure
? '未抛出异常(不符合预期),返回: ${_truncate(value)}'
: _truncate(value),
elapsed: '${sw.elapsedMilliseconds} ms',
icon: expectFailure
? Icons.error_outline
: Icons.check_circle,
);
} catch (e) {
sw.stop();
r = _ScenarioResult(
title: title,
success: expectFailure,
errorPath: expectFailure,
detail: expectFailure
? '按预期抛出异常: $e'
: '非预期异常: $e',
elapsed: '${sw.elapsedMilliseconds} ms',
icon: expectFailure ? Icons.block : Icons.error_outline,
);
}
if (mounted) setState(() => _results.add(r));
}
String _truncate(String v) => v.length > 160 ? '${v.substring(0, 160)}…' : v;
Widget build(BuildContext context) {
return MaterialApp(
title: 'signed_json OHOS demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: Scaffold(
appBar: AppBar(
title: const Text('signed_json OpenHarmony 演示'),
actions: [
IconButton(
icon: const Icon(Icons.replay),
onPressed: _running ? null : _runAll,
tooltip: '重新运行',
),
],
),
body: ListView(
padding: const EdgeInsets.all(8),
children: [
for (final r in _results)
Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
r.icon,
color: r.errorPath
? Colors.red
: (r.success ? Colors.green : Colors.red),
size: 22,
),
const SizedBox(width: 8),
Expanded(
child: Text(r.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600)),
),
Text(r.elapsed,
style: const TextStyle(
color: Colors.grey, fontSize: 12)),
],
),
const SizedBox(height: 8),
SelectableText(
r.detail,
style: TextStyle(
fontSize: 13,
color: r.success ? Colors.black87 : Colors.red),
),
],
),
),
),
if (_results.isEmpty)
const Padding(
padding: EdgeInsets.all(32),
child: Center(child: CircularProgressIndicator()),
),
],
),
),
);
}
}
class _ScenarioResult {
final String title;
final bool success;
final bool errorPath;
final String detail;
final String elapsed;
final IconData icon;
const _ScenarioResult(
{required this.title,
required this.success,
required this.errorPath,
required this.detail,
required this.elapsed,
required this.icon});
}
每个场景都必须显式传 <Map<String, dynamic>>,否则 parseAndDecode<T> 内部的 jsonDecode(response) as T 会因为推不出目标类型而失败(旧版示例仅写 _signedJson.verify(...) 时,运行时报 type '_Map<String, dynamic>' is not a subtype of type 'FutureOr<String>' in type cast,见 9.5)。
chainEncoded 不是仓库自带的数据,它由 example/tool/ohos_chain_test.dart 在宿主端用 config/keys/sign.jwk 私钥对 zlib(remote_config.enc) 签名后生成;演示前需要先在宿主机跑一次该脚本,把输出写回 chainEncoded。该脚本同时会在宿主端验证三个场景(JWS 验签、JWE 解密、先验签后解密的链),可以提前排除密钥配置错误。
7.4 页面退出时的异步处理
异步回调先检查 mounted,避免页面销毁后继续调用 setState。verify / decrypt / verifyAndDecrypt 都是一次性调用,没有订阅需要取消,dispose 中无需额外清理。
多个页面都需要 JWS / JWE 处理时,可以在应用启动阶段缓存一个 SignedJson 实例并持有密钥字符串,各页面只调用 API,避免重复构造 / 解析 JWK。
八、验证、构建与鸿蒙设备运行效果
8.1 分别验证插件与 example
从插件仓库根目录执行:
flutter pub get
flutter analyze
flutter test
cd example
flutter pub get
flutter analyze
flutter test
接口测试应验证 verify / decrypt / verifyAndDecrypt 的泛型类型、参数传递、返回值解析以及异常路径。本例的 test/signed_json_test.dart 通过 mock signed_json 通道,验证 verify<Map<String, dynamic>> / decrypt<Map<String, dynamic>> 返回 mock 值;上游历史示例曾把 _signedJson.verify(...) 当作返回 String 使用,运行时会因 parseAndDecode<T> 的 jsonDecode(response) as T 强转失败而崩溃(参见 9.5)。
当前仓库的运行结果为:flutter analyze 在根目录与 example 目录下合计报告 0 error、42 issue(其中多数为 example/tool/ 调试脚本里的 avoid_print,以及 test/signed_json_test.dart 中的 deprecated_member_use 提示)。flutter test 在本机因 flutter_tester 的 WebSocketException: Invalid WebSocket upgrade request 无法启动,这是本地测试环境问题,不是被测代码缺陷。第六章适配分支上的真机运行已经覆盖了核心场景,详细的鸿蒙设备日志保留在 docs/ohos-test-evidence/evidence_hilog_full.txt。
8.2 确认设备连接
hdc list targets
flutter devices
设备首次连接电脑时,需要在手机端确认调试授权。本文适配使用的设备为 4UQ9K25508013016,系统版本 Ohos OpenHarmony-6.1.1.120(API 24),架构 ohos-arm64。列表为空时,检查 USB 连接、调试模式和电脑授权。
8.3 配置签名
真机安装的 HAP 通常需要有效签名。推荐使用 DevEco Studio 为 entry 模块配置自动签名:
- 用 DevEco Studio 打开
example/ohos,不是仓库根目录; - 等待工程 Sync 成功,确认 Project 视图中存在
entry模块; - 打开 File > Project Structure > Signing Configs;
- 为
defaultproduct 选择或生成签名; - 确认设备、应用包名(应已改为
com.example.signed_json_example,参见 5.3.1)、证书和 Profile 匹配; - 再回到终端执行 Flutter 构建或运行。
签名材料保存在本机,公开仓库中只保留构建所需的通用配置。
8.4 运行示例
以下命令在 example/ 目录执行,将 <device-id> 替换为设备列表中的实际 ID:
flutter run -d <device-id>
也可以先构建 HAP:
flutter build hap --debug
典型产物位于:
example/ohos/entry/build/default/outputs/default/
目录中通常包含已签名和未签名 HAP。真机安装应选择与当前设备匹配的已签名产物。
8.5 在设备上测试六个场景
- 打开应用,确认初始页面渲染 6 个场景卡片,3 个正向用绿色对勾(
Icons.check_circle),3 个负向显示红色禁止图标(Icons.block); - 每个卡片右侧显示耗时(毫秒),下面给出运行结果摘要:
- 1
verify:{"some_text":"Something Something"},300500 ms; - 2
decrypt:同上,1030 ms; - 3
verifyAndDecrypt:同上,150250 ms; - 4
verify(篡改签名):按预期抛出FormatException或JoseException: Could not decrypt/verify payload; - 5
verify(错误证书):按预期抛出JoseException: Could not decrypt/verify payload; - 6
decrypt(错误证书):按预期抛出JoseException: Could not decrypt/verify payload。
- 1
- 点击右上角“重新运行”按钮,确认所有场景重新执行,结果稳定可复现;
- 切到后台再切回前台,确认页面状态不丢失;
- 卸载后重新安装,确认无缓存副作用。
页面显示的耗时来自 Stopwatch().elapsedMilliseconds,覆盖 _runAll → compute() → package:jose 全链路;compute() 跨 isolate 带来的固定开销主要体现在场景 1 和场景 3。
8.6 鸿蒙设备运行效果
完成适配后,Flutter 应用能够通过纯 Dart 分支完成 JWS 验签和 JWE 解密,覆盖 3 个正向场景和 3 个预期失败的负向场景。下面是在 OpenHarmony 6.1.1.120(API 24)真机上的两次运行截图:
| 序号 | 测试项 | 结果表现 | 耗时 | 状态 |
|---|---|---|---|---|
| 1 | verify:用 verify.jwk 公钥验签 remote_config.signed | 验签通过,返回 {“some_text”:“Something Something”} | 3312 ms | 通过 |
| 2 | decrypt:用 decrypt.jwk 私钥解密 remote_config.enc | 解密成功,返回 {“some_text”:“Something Something”} | 692 ms | 通过 |
| 3 | verifyAndDecrypt:先验签再解密的完整链路 | 完整链路成功,返回 {“some_text”:“Something Something”} | 167 ms | 通过 |
| 4 | verify(篡改签名):签名最后一个字符被翻转,应验签失败 | 按预期抛出异常:JoseException: Could not decrypt/verify payload | 106 ms | 按预期失败 |
| 5 | verify(错误证书):用不相关的 EC 公钥,应验签失败 | 按预期抛出异常:JoseException: Could not decrypt/verify payload | 3 ms | 按预期失败 |
| 6 | decrypt(错误证书):用不相关的 RSA 私钥,应解密失败 | 按预期抛出异常:JoseException: Could not decrypt/verify payload | 3 ms | 按预期失败 |
以下是操作的视屏,可以参考一下:
| 首次运行 | 最终运行 |
|---|---|
| 6 个场景全部按预期:3 正向返回原文 JSON,3 负向抛出异常并以红色禁止图标显示 | 同样 6 个场景,3 正向保持绿色对勾,3 负向保持红色禁止图标 |
截图来源:docs/ohos-test-evidence/evidence2_full_scenarios.jpeg | 截图来源:docs/ohos-test-evidence/evidence3_final_red_icons.jpeg |
两次截图均保留在仓库的 docs/ohos-test-evidence/ 目录下,便于评审时核对。日志捕获保留在 evidence_hilog_full.txt,可以按需筛 signed_json 相关条目定位问题。负向场景的红色禁止图标说明 demo 已经识别“预期的异常”,而不是把负向测试当作失败;如果图标显示为红色 error_outline,说明 demo 把异常当成了非预期失败,需要排查 JWK 配置是否正确。
九、FAQ:适配过程与使用问题
9.1 Missing SDK components
典型错误如下:
Missing SDK components. SDK path: ...,
missing components: toolchains,ets,js,native,previewer.
这个错误发生在 Hvigor 同步阶段。通常需要检查构建工具使用的 SDK 路径、组件是否完整,以及 Hvigor 与 SDK 的版本是否匹配。
处理顺序:
- 在 DevEco Studio SDK Manager 中确认 API 26 组件已经下载完整;
- 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
- 避免误用
/Applications/DevEco-Studio.app/Contents/sdk之类的不完整目录; - 确认 SDK 根目录下存在
toolchains、ets、js、native、previewer; - 执行
flutter config --ohos-sdk <正确路径>; - 重新执行
flutter doctor -v和 DevEco Studio Sync。
因为 Sync 和 Compile 首先读取 Mac 本地 SDK。手机 API 版本只在部署、安装和运行时参与兼容判断。即使完全不连接手机,本地 SDK 不完整时也会得到相同错误。
当前工程的 compatibleSdkVersion 是 API 18,因此 API 24 在安装版本门槛上是满足的;但 JWE 解密依赖应用安装包内携带的 JWK 私钥字符串,仍需在目标应用上验证。
9.2 DevEco Studio 中看不到 entry 模块
本库没有生成插件 HAR,示例应用的 entry 模块位于 example/ohos/entry。
请直接使用 DevEco Studio 打开:
flutter-signed-json/example/ohos
如果仍看不到 entry,先解决 SDK Sync 错误,再检查 example/ohos/build-profile.json5 的 modules 是否包含 ./entry。同步失败时,Project Structure 无法正确解析模块,签名界面也可能不显示 entry。
9.3 无法手动签名
签名配置依附于可构建的应用模块和 product。只有 entry 模块、工程 Sync 失败,或者打开了错误目录时,DevEco Studio 都可能无法提供 entry 签名入口。
建议先确认:
- 打开的是
example/ohos,不是仓库根目录; - SDK 组件完整并且 Sync 成功;
entry的模块类型为entry;defaultproduct 和 target 已正确关联;- 当前账号、证书和调试设备状态有效;
example/ohos/AppScope/app.json5的bundleName已是当前示例的com.example.signed_json_example,没有残留keyboardheightpluginexample。
9.4 OHOS 上需要写原生代码吗?
不需要。signed_json 的平台分支完全在 Dart 侧:
bool get useNativeSignedJson => Platform.isAndroid;
OHOS 既不是 Android 也不是 iOS,useNativeSignedJson 始终为 false,方法通道 signed_json 不会被触发。所有 JWS / JWE 处理都通过 compute() 在后台 isolate 跑 package:jose + ZLibCodec 完成。适配本库时:
- 不要在仓库根目录创建
ohos/或写SignedJsonPlugin.ets; - 不要在
pubspec.yaml的flutter.plugin.platforms下新增ohos:条目; - 一切 OHOS 相关工作集中在
example/ohos/:修正AppScope/app.json5的bundleName、清理build-profile.json5的签名材料、覆盖示例 demo。
如果未来插件作者在 OHOS 上加了原生实现(例如用 HarmonyOS 的 JOSE 库),需要再补一个 ArkTS 插件并相应调整 useNativeSignedJson 的判断;本文不涉及这部分。
9.5 报 type '_Map<String, dynamic>' is not a subtype of type 'FutureOr<String>'
这是上游示例的历史坑。signed_json 的三个方法都是 Future<T>,返回值由 parseAndDecode<T> 决定:
T parseAndDecode<T>(String response) => jsonDecode(response) as T;
调用方不指定泛型时,T 会被推断为 dynamic 或默认类型;当 JSON 实际是 Map<String, dynamic> 时,强转会失败。运行期错误形如:
type '_Map<String, dynamic>' is not a subtype of type 'FutureOr<String>' in type cast
修复方式是显式提供泛型:
await _signedJson.verify<Map<String, dynamic>>(encoded);
await _signedJson.decrypt<Map<String, dynamic>>(encryptedEncoded);
await _signedJson.verifyAndDecrypt<Map<String, dynamic>>(chainEncoded);
旧版示例直接把返回值当作 String 使用,因此新版 demo 把 6 个场景都改成显式 <Map<String, dynamic>>。运行时遇到的崩溃截图保留在 docs/ohos-test-evidence/evidence_error_initial.jpeg,与当前 demo 的 evidence2_full_scenarios.jpeg 对照可以看到差异。
9.6 bundleName 还是 com.example.keyboardheightpluginexample
flutter create --platforms=ohos . 在示例工程下生成的 AppScope/app.json5 默认会从上一次执行 flutter create 的目录继承 bundleName。如果当前目录是从另一个插件模板(比如 keyboardheightpluginexample)拷过来的,新工程的 bundleName 仍然是旧值。
直接使用旧 bundleName 的后果:
- 与示例工程名(
signed_json_example)不一致; - 与设备上已安装的
keyboardheightpluginexampleDemo 冲突; - 签名 / Profile 不匹配,会出现 9.7 中的安装失败。
修正方式见 5.3.1:
{
"app": {
"bundleName": "com.example.signed_json_example",
"vendor": "example",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:app_icon",
"label": "$string:app_name"
}
}
修改后用 git diff 确认仓库其它位置没有再硬编码 keyboardheightpluginexample。
9.7 SDK 约束是 >=2.15.0 <3.0.0,OHOS 工具链需要改成 >=3.0.0 吗?
不需要。仓库的 pubspec.yaml(以及 example/pubspec.yaml)仍然使用:
environment:
sdk: ">=2.15.0 <3.0.0"
flutter: ">=2.5.0"
实测在 Flutter 3.44.9+ohos-0.0.1-canary1(Dart 3.12.2)下 flutter pub get 可以解析成功。package:jose ^0.3.2 和 package:computer ^3.2.1 都是纯 Dart 包,不依赖特定 Dart 主版本。强行改成 >=3.0.0 反而会失去对老版本工具链的兼容。建议保持现状,等上游主版本升级时再统一调整。
9.8 编译成功但安装失败
常见原因包括:
- HAP 未签名或使用了错误的 Profile;
- 设备未加入调试设备列表;
AppScope/app.json5的bundleName与签名 Profile 不匹配(参见 9.6);- 安装包的
compatibleSdkVersion高于设备 API; - 手机上已经安装了使用不同证书签名的同包名应用。
根据安装错误码区分签名、版本和包名冲突,再处理对应配置。flutter pub get 解析阶段应当不再报与签名相关的问题,因为本库没有 ohos: 平台条目。
9.9 flutter create 不认识 ohos,或包名不合法
先执行 flutter --version,确认使用的是 OHOS 版工具链,环境配置回到第二节的社区链接核对。本例在 example/ 下执行 flutter create --platforms=ohos . 时不需要再传 --project-name,因为 example/pubspec.yaml 已经声明包名 signed_json_example;如果改成在仓库根目录执行 flutter create --template=plugin --platforms=ohos --project-name signed_json .,则会强行生成空的 ohos/ HAR 脚手架,请避免这种情况。
包名报错时,确认当前目录包含目标插件的 pubspec.yaml,并显式传入 --project-name signed_json;仓库名 flutter-signed-json 不能直接作为 Dart 包名(pubspec.yaml 的 name 是 signed_json)。
9.10 MissingPluginException:纯 Dart 实现为何不会抛
MissingPluginException 通常表示 Dart 通道找不到已注册的原生插件。本库在 OHOS 上不调用任何原生通道,因此即使 GeneratedPluginRegistrant.ets 为空(参见 5.4),也不会抛该异常。
如果出现了 MissingPluginException,先排查是否被某个依赖插件触发了 MethodChannel 调用,例如 cupertino_icons 或调试期加入的统计 SDK。本例 example/pubspec.yaml 仅依赖 cupertino_icons,没有其它 OHOS 通道需要注册。
9.11 场景跑出异常时,如何对照原仓库密钥复现
如果真机上 1 / 2 / 3 场景也失败:
- 确认
verifyCert/decryptCert/encryptedEncoded/encoded/chainEncoded与config/下对应文件一致; - 在宿主机端运行
dart run example/tool/ohos_chain_test.dart,它会用config/keys/下的真实 JWK 在宿主端跑 S1(验签)/ S2(解密)/ S4(链式),输出成功或异常; - 如果宿主端 S1 / S2 / S4 失败,说明示例中的常量抄错了,或
config/keys/*.jwk被改动过;如果宿主端成功而 OHOS 失败,再检查example/lib/main.dart的常量是否与宿主端脚本一致; chainEncoded必须由ohos_chain_test.dart现场生成,不能从其它示例拷贝;不同sign.jwk私钥产生的 JWS 完全不同。
docs/ohos-test-evidence/ 下的截图和 evidence_hilog_full.txt 是评估适配是否成功的关键证据,评审前应一并提交。
相关链接
更多推荐




所有评论(0)