开发工具: 华为云码道

本文配套仓库: icapps/flutter-signed-json
鸿蒙适配后仓库:https://atomgit.com/oh-flutter/flutter-signed-json

本文配套仓库:https://atomgit.com/CPF-Flutter/fluttertpc_signed_json(TAG:0.0.2-ohos-1.0.0,分支:master),文中示例代码位于仓库 example/ 目录。

在这里插入图片描述

什么是签名 JSON(JWS/JWE)? 在配置分发、远程下发场景中,服务端需要将业务 JSON 数据安全地传递给客户端。直接传输明文 JSON 存在被篡改和泄露的风险。签名 JSON(JSON Web Signature,JWS)用私钥对 JSON 做数字签名,客户端用公钥验签确认数据完整性;加密 JSON(JSON Web Encryption,JWE)用公钥对 JSON 加密,客户端用私钥解密获取原文。两者可链式组合——外层签名保护完整性,内层加密保护机密性,形成「先签后解」的双重安全链路。

把服务端下发的签名 JSON 在客户端验签并解密,是远程配置分发、安全策略下发、许可证校验场景下的高频需求:用 ES512 公钥验签 JWS 确认数据未被篡改、用 RSA 私钥解密 JWE 获取业务 JSON、先验签再解密完成完整链路。鸿蒙应用同样需要这个能力。本文介绍如何使用鸿蒙化适配后的 Flutter 三方库 signed_json,用 verify / decrypt / verifyAndDecrypt 三个方法在鸿蒙 App 内完成 JWS 验签、JWE 解密与链式调用,全部走纯 Dart 路径零权限,并附上 OpenHarmony-6.1.1.120 真机的完整实测记录。

一、最终运行效果

应用启动后,页面自动运行六个测试场景并以卡片列表展示结果:验签成功(绿色勾号)、解密成功(绿色勾号)、链式调用成功(绿色勾号)、篡改签名验签失败(红色叉号,按预期抛出异常)、错误证书验签失败(红色叉号,按预期抛出异常)、错误证书解密失败(红色叉号,按预期抛出异常)。每张卡片显示场景标题、耗时与结果详情。

验证点结果
应用启动,Flutter 页面正常渲染六场景卡片通过
场景一:verify 验签 remote_config.signed 成功,返回业务 JSON通过
场景二:decrypt 解密 remote_config.enc 成功,返回业务 JSON通过
场景三:verifyAndDecrypt 链式调用成功,返回最终业务 JSON通过
场景四:篡改签名最后一个字符,验签按预期失败抛出异常通过
场景五:用不相关 EC 公钥验签,按预期失败抛出异常通过
场景六:用不相关 RSA 私钥解密,按预期失败抛出异常通过
全程无需申请任何敏感权限通过

KeyHash 示例页

鸿蒙技术点:FlutterPage 与 XComponent 渲染管线
鸿蒙侧的 Flutter 渲染入口是 FlutterPage 组件,它在 Index.ets 中被 @Entry 组件的 build() 方法直接使用。FlutterPage 内部封装了 XComponent——OpenHarmony 提供的底层渲染画布组件。XComponent 通过 NAPI 桥接 C++ 引擎层,将 Flutter 的 Skia 渲染管线挂载到鸿蒙的渲染树中,使 Dart 层的 Widget 树(包括六场景卡片列表)在鸿蒙设备上完整渲染。FlutterAbility 作为容器 Ability,管理 FlutterEngine 的生命周期,在 configureFlutterEngine 中注册所有平台插件。

二、signed_json 是什么

signed_json 原库(pub.dev 0.0.2,作者 icapps)是一个用于读取、校验并解密签名 JSON(JWS/JWE)的 Flutter 库。它配合 sign_config 工具生成的签名产物使用:用公钥(JWK)对 Base64 编码的 JWT 做 ES512(ECDSA P-521)验签,可选地用私钥做 RSA1_5 解密,最后对载荷做 ZLib 解压,返回业务 JSON。鸿蒙适配版在其基础上新增了 OpenHarmony / HarmonyOS 平台支持:Dart 层零改动,验签与解密全部走纯 Dart 路径(jose 库 + ZLibCodec),OHOS 平台原生插件为声明式桩实现。

几个对使用者友好的特点:

  • 纯 Dart 加解密:ES512 验签、RSA 解密、ZLib 解压全部由 Dart 标准库与 jose 库完成,不调用任何系统 API,逻辑在所有平台完全一致;
  • 零权限:OHOS 平台桩仅注册通道契约,实际加解密不经过原生通道,不需要在 module.json5 中申请任何权限;
  • Isolate 后台计算:通过 compute() 将验签与解密放到后台 Isolate 执行,避免阻塞 UI 线程;
  • 篡改检测:签名被修改任何一个字符都会导致验签失败并抛出异常,确保数据完整性;
  • 链式安全:verifyAndDecrypt 先验签外层 JWS 得到内层 JWE,再解密得到最终 JSON——「外层签名保护完整性,内层加密保护机密性」的双重安全链路;
  • 跨平台一套代码:Android(原生实现)、iOS、鸿蒙、Web 共用同一套 Dart API,运行在哪端就按哪端的方式加解密。

接口说明

SignedJson 构造器
名称描述类型参数类型返回值必填鸿蒙平台支持
verificationCert验签公钥 JWK 字符串属性String—是是
decryptionCert解密私钥 JWK 字符串(可选)属性String?—否是
SignedJson 方法
名称描述类型参数类型返回值必填鸿蒙平台支持
verify<T>对 JWS 串验签,返回解压并解析后的 JSON方法String encodedFuture是是
decrypt<T>对 JWE 串解密,返回解压并解析后的 JSON方法String encodedFuture是是
verifyAndDecrypt<T>先验签再解密,返回最终业务 JSON方法String encodedFuture是是

各平台加解密路径

平台验签路径解密路径说明
Android原生(MethodChannel)原生(MethodChannel)Platform.isAndroid 为 true,走原生实现
iOS纯 Dart(jose + ZLib)纯 Dart(jose + ZLib)useNativeSignedJson 为 false
OpenHarmony / HarmonyOS纯 Dart(jose + ZLib)纯 Dart(jose + ZLib)Platform.isAndroid 为 false
Web纯 Dart(jose + ZLib)纯 Dart(jose + ZLib)Web 桩为声明式

三、环境准备

本文所有实测均在以下环境完成:

项版本说明
Flutter(ohos 版)3.44.9+ohos-0.0.1-canary1主验证环境,真机实测
DevEco Studio26.0.0(26.0.0.821)构建环境
编译 SDK5.1.0(18)宿主工程 compatibleSdkVersion 同值,保留带括号的旧格式
真机OpenHarmony-6.1.1.120(API 24)ohos-arm64

鸿蒙技术点:compatibleSdkVersion 与 API Level 的对应关系
compatibleSdkVersion 是鸿蒙工程 build-profile.json5 中的关键字段,声明应用的最低兼容 API 版本。鸿蒙的 API 版本与系统版本一一对应:5.1.0(18) 对应 API 18,6.1.0(23) 对应 API 23,6.1.1.120 对应 API 24。真机安装时,系统会校验应用的 compatibleSdkVersion 不高于设备实际 API 版本,否则报"此应用暂不支持在当前设备安装"。本文 example 工程设为 5.1.0(18),在 API 24 真机上可正常安装运行。

两点提醒:

  • 编译鸿蒙目标需要使用 ohos 版 Flutter SDK,普通 Flutter SDK 无法编译 ohos 产物;
  • 若在真机上安装应用报"此应用暂不支持在当前设备安装",是宿主工程的 compatibleSdkVersion 高于设备 API 导致的,与插件无关,处理方式见 FAQ Q3。

四、引入依赖

进入工程目录,在 pubspec.yaml 中添加 git 依赖:

dependencies:
  signed_json:
    git:
      url: https://atomgit.com/CPF-Flutter/fluttertpc_signed_json
      ref: 0.0.2-ohos-1.0.0

执行命令拉取依赖:

flutter pub get

TAG 命名规则:原库版本-ohos-版本号。

Flutter 框架版本TAG 名称分支名
3.440.0.2-ohos-1.0.0master

说明:该 TAG 已在 Flutter 3.44.9+ohos-0.0.1-canary1 + OpenHarmony-6.1.1.120(API 24)真机上实测通过。compatibleSdkVersion 设为 5.1.0(18) 即可在 API 24 真机安装运行。库依赖 jose ^0.3.2(JOSE 协议 Dart 实现)和 computer ^3.2.1(Isolate 并发工具),均由纯 Dart 实现。

五、代码接入

5.1 导入库

import 'dart:convert';
import 'package:signed_json/signed_json.dart';

导入后即可使用 SignedJson 类。该类接收验签公钥(JWK 字符串)和可选的解密私钥(JWK 字符串),提供 verify / decrypt / verifyAndDecrypt 三个方法。

5.2 构造 SignedJson 实例

/// verificationCert 为验签公钥 JWK(ES512 / P-521)
static const verifyCert = '''{
  "kty": "EC",
  "use": "sig",
  "crv": "P-521",
  "kid": "signed_json",
  "x": "AT0MMaUvyZdRZaCIqkLZPks0NE4kPZhgTwIivnr0tDhcl7Ao9SSdiXPuCsZS3HaAuQq5Kk1sSWGwNtteiq5JsiSg",
  "y": "AVKxKaIYJewNWA3DvD5qzpW9vSMt6A1crUqVpQ-MKAOVsCL_RwwvQoedRoMbYpf6T7XLflECaI58pBtv8JfF8zVb",
  "alg": "ES512"
}''';

/// decryptionCert 为解密私钥 JWK(RSA,可选)
static const decryptCert = '''{
  "kty": "RSA",
  "n": "lgtcwDDTKxWLLnpVTGCad7LWnRP...",
  "e": "AQAB",
  "d": "hlv6XGfcUNJj5eTKCTDgYZuVrucW9Q...",
  "alg": "RSA1_5"
}''';

final signedJson = SignedJson(verifyCert, decryptionCert: decryptCert);

verifyCert 是 EC(椭圆曲线)公钥的 JWK 表示,算法为 ES512(ECDSA P-521)。decryptCert 是 RSA 私钥的 JWK 表示,算法为 RSA1_5。两个密钥均可通过 sign_config 工具或 mkjwk.org 生成。

代码逐段分析:JWK 密钥格式
JWK(JSON Web Key)是一种用 JSON 表示密钥的标准格式。EC 类型的 JWK 包含 kty(密钥类型,EC)、crv(曲线,P-521)、x 和 y(公钥坐标,Base64URL 编码)、alg(算法,ES512)。RSA 类型的 JWK 包含 kty(RSA)、n(模数)、e(指数,通常 AQAB 即 65537)、d(私钥指数)等参数。jose 库的 JsonWebKey.fromJson 方法将 JWK JSON 解析为可用的密钥对象,用于验签或解密操作。

5.3 验签(verify)

/// JWS 签名串(Compact Serialization 格式)
static const encoded =
    'eyJhbGciOiJFUzUxMiIsImtpZCI6InNpZ25lZF9qc29uIn0.eNqr5lJQUCrOz02NL0mtKFGyUlAKBnJKMjLz0hXgLCWuWgALBg2D.AXMx-2iQVtaDIaV14lXhw7j6hW0D0HKyWYu9kUz1W4W0j8gOlDU2YWipyEDWGAGXKgdmIpWl1SeWrVlXdJeVLYFlAe2pqriYxevIzEErRdP1VwLDP7lCiLERCaiz_usAAY2fiHVNqEqFhPr8bxpnWIxiEcoG2BB5zUEoEH_kZ51I0jgV';

final Map<String, dynamic> config =
    await signedJson.verify<Map<String, dynamic>>(encoded);
print(jsonEncode(config));

verify 方法接收 JWS 签名串(Compact Serialization 格式,三段以 . 分隔:Header.Payload.Signature),用 verificationCert 公钥验签。验签通过后,对 Payload 做 ZLib 解压并 jsonDecode 返回业务 JSON。验签失败(签名被篡改或证书不匹配)时抛出异常。

代码逐段分析:_verifyOnBackgroundThread 验签流程
验签在后台 Isolate 中执行(通过 compute() 调度)。流程分五步:第一步,JsonWebSignature.fromCompactSerialization(encoded) 将 JWS 签名串解析为 JWS 对象。第二步,JsonWebKey.fromJson(jsonDecode(cert)) 将验签公钥 JWK 解析为密钥对象。第三步,JsonWebKeyStore()..addKey(jwk) 将公钥添加到密钥库。第四步,jws.getPayload(keyStore) 验证签名并提取 Payload(若签名不匹配则抛出异常)。第五步,ZLibCodec().decoder.convert(payload.data) 对 Payload 做 ZLib 解压,utf8.decode(unzipped) 转为字符串返回。

5.4 解密(decrypt)

/// JWE 加密串(Compact Serialization 格式)
static const encryptedEncoded =
    'eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiUlNBMV81In0.a5a73YZgaQw...';

final Map<String, dynamic> decrypted =
    await signedJson.decrypt<Map<String, dynamic>>(encryptedEncoded);
print(jsonEncode(decrypted));

decrypt 方法接收 JWE 加密串(Compact Serialization 格式,五段以 . 分隔:Header.EncryptedKey.IV.Ciphertext.AuthenticationTag),用 decryptionCert 私钥解密。解密后同样做 ZLib 解压并 jsonDecode 返回业务 JSON。构造 SignedJson 时未提供 decryptionCert 而调用 decrypt 会抛出 ArgumentError。

代码逐段分析:_decryptOnBackgroundThread 解密流程
解密同样在后台 Isolate 中执行。流程与验签类似:JsonWebEncryption.fromCompactSerialization(encoded) 解析 JWE 对象,JsonWebKey.fromJson 解析私钥,jwe.getPayload(keyStore) 用私钥解密并提取 Payload,ZLibCodec 解压后返回字符串。JWE 的解密过程涉及 RSA 解密对称密钥、AES 解密载荷两个阶段——外层 RSA1_5 解密出对称密钥,内层 A128CBC-HS256 用对称密钥解密实际载荷。

5.5 先验签再解密(verifyAndDecrypt)

/// 链式签名串:外层 JWS 签名,Payload 为内层 JWE 加密串
static const chainEncoded =
    'eyJhbGciOiJFUzUxMiIsImtpZCI6InNpZ25lZF9qc29uIn0.eJwFwce2Q0AAANAvko...';

final Map<String, dynamic> chained =
    await signedJson.verifyAndDecrypt<Map<String, dynamic>>(chainEncoded);
print(jsonEncode(chained));

verifyAndDecrypt 方法实现了「先签后解」的完整链路:先用 verificationCert 验签外层 JWS,得到内层 JWE 加密串;再用 decryptionCert 解密内层 JWE,得到最终业务 JSON。这种模式适用于「外层签名保护完整性,内层加密保护机密性」的配置分发场景。

代码逐段分析:verifyAndDecrypt 链式调用
链式调用分两阶段。第一阶段,调用 internalVerify(certVerify, encoded) 验签外层 JWS——与 verify 方法相同的流程,返回内层 JWE 加密串。第二阶段,调用 internalDecrypt(certDecrypt, 内层JWE串) 解密内层 JWE——与 decrypt 方法相同的流程,返回最终业务 JSON 字符串。最后通过 compute(parseAndDecode, result) 将 JSON 字符串解析为 Dart 对象返回。两阶段均在后台 Isolate 中执行,避免阻塞 UI 线程。

5.6 篡改检测

/// 篡改签名:将 encoded 最后一个字符从 'V' 改为 'g'
static const tamperedEncoded =
    'eyJhbGciOiJFUzUxMiIsImtpZCI6InNpZ25lZF9qc29uIn0.eNqr5lJQUCrOz02NL0mtKFGyUlAKBnJKMjLz0hXgLCWuWgALBg2D.AXMx-2iQVtaDIaV14lXhw7j6hW0D0HKyWYu9kUz1W4W0j8gOlDU2YWipyEDWGAGXKgdmIpWl1SeWrVlXdJeVLYFlAe2pqriYxevIzEErRdP1VwLDP7lCiLERCaiz_usAAY2fiHVNqEqFhPr8bxpnWIxiEcoG2BB5zUEoEH_kZ51I0jgg';

try {
  await signedJson.verify<Map<String, dynamic>>(tamperedEncoded);
} catch (e) {
  print('验签失败(符合预期): $e');
}

JWS 签名串的第三段(Signature)是 ECDSA 签名值,修改任何一个字符都会导致签名验证失败。jose 库在验签时检查签名有效性,不匹配时抛出异常——这是签名 JSON 防篡改的核心保障。

鸿蒙技术点:Isolate 在鸿蒙上的行为一致性
compute() 是 Flutter Foundation 层提供的 Isolate 并发工具,将耗时计算放到后台 Isolate 执行。在鸿蒙平台上,Flutter Engine 的 C++ 层完整实现了 Dart Isolate 的创建、消息传递与销毁机制。compute 内部通过 Isolate.run 创建新的 Isolate,将参数序列化传递过去,计算完成后将结果序列化传回主 Isolate。这意味着 jose 库的 ES512 验签和 RSA 解密在鸿蒙上与 Android/iOS 行为完全一致——Isolate 隔离的内存空间、独立的事件循环、消息传递的序列化语义全部由引擎层保证。

5.7 实战:远程配置安全分发

实际业务中常见的场景是服务端将业务配置签名加密后下发,客户端验签解密后使用。下面是一个可直接使用的组件:

class RemoteConfigLoader {
  final SignedJson _signedJson;

  RemoteConfigLoader(String verifyCert, String decryptCert)
      : _signedJson = SignedJson(verifyCert, decryptionCert: decryptCert);

  /// 加载远程配置:先验签确保完整性,再解密获取配置内容
  Future<Map<String, dynamic>> load(String signedAndEncrypted) async {
    try {
      return await _signedJson.verifyAndDecrypt<Map<String, dynamic>>(
          signedAndEncrypted);
    } on ArgumentError catch (e) {
      throw Exception('解密私钥未配置: $e');
    } catch (e) {
      throw Exception('配置验签或解密失败,数据可能被篡改: $e');
    }
  }

  /// 仅验签加载(无加密场景)
  Future<Map<String, dynamic>> loadSigned(String signed) async {
    try {
      return await _signedJson.verify<Map<String, dynamic>>(signed);
    } catch (e) {
      throw Exception('配置验签失败,数据可能被篡改: $e');
    }
  }
}

验签或解密失败时,异常信息明确提示"数据可能被篡改",业务可据此降级处理(如使用本地默认配置、拒绝加载等)。该组件可直接嵌入应用启动流程,在配置下发后安全加载。

六、运行与验证

以下为 demo 工程的真机实测记录。仓库中 example 的 Bundle Name 为 com.example.signed_json_example。

设备项值
机型OpenHarmony 真机
系统版本OpenHarmony-6.1.1.120
API 版本24
构建环境Flutter 3.44.9+ohos-0.0.1-canary1, DevEco Studio 26.0.0

6.1 验证一:验签成功场景

安装、启动 demo:

# 构建 hap 后安装
hdc install entry-default-signed.hap

# 启动 demo
hdc shell aa start -b com.example.signed_json_example -a EntryAbility

应用启动后,initState 调用 _runAll() 依次执行六个场景。场景一「verify 验签」:用 verifyCert 公钥对 encoded JWS 串验签,验签通过后 ZLib 解压 Payload,jsonDecode 返回业务 JSON。卡片显示绿色勾号,详情展示截断后的 JSON 内容,耗时在毫秒级别。

在这里插入图片描述

6.2 验证二:解密与链式调用

# 查看运行日志
hdc shell hilog | grep signed_json_demo

场景二「decrypt 解密」:用 decryptCert 私钥对 encryptedEncoded JWE 串解密,解密成功后返回业务 JSON。场景三「verifyAndDecrypt 链式调用」:先验签外层 JWS 得到内层 JWE,再解密内层 JWE 得到最终 JSON——两阶段均成功。日志输出:

signed_json_demo | 2. decrypt => OK {"key":"value",...}
signed_json_demo | 3. verifyAndDecrypt => OK {"key":"value",...}

在这里插入图片描述

6.3 验证三:篡改与错误证书检测

场景四「篡改签名」:将 encoded 最后一个字符从 V 改为 g,jose 库验签时检测到签名不匹配,抛出异常。卡片显示红色叉号,详情为"按预期抛出异常"。

场景五「错误证书」:用不相关的 EC 公钥 wrongVerifyCert 验签 encoded,公钥与签名不匹配,抛出异常。

场景六「错误解密证书」:用不相关的 RSA 私钥 wrongDecryptCert 解密 encryptedEncoded,私钥与密文不匹配,抛出异常。

日志输出:

signed_json_demo | 4. verify(篡改签名) => EXCEPTION ...
signed_json_demo | 5. verify(错误证书) => EXCEPTION ...
signed_json_demo | 6. decrypt(错误证书) => EXCEPTION ...

在这里插入图片描述

6.4 验证四:插件注册日志

通过 hdc hilog 抓取运行日志:

hdc shell hilog -r
hdc shell aa start -b com.example.signed_json_example -a EntryAbility
hdc shell hilog | grep SignedJsonPlugin

日志输出:

SignedJsonPlugin: Attached to engine (OHOS 使用纯 Dart 路径)

插件由 GeneratedPluginRegistrant 注册成功,MethodChannel('signed_json') 通道就绪。但由于 useNativeSignedJson 在鸿蒙上为 false(Platform.isAndroid 为 false),Dart 层不经过原生通道,直接走纯 Dart 路径(jose 库验签解密 + ZLibCodec 解压)。原生插件仅保证注册表完整与通道契约对齐。

实测结论:

验证点结果
应用启动,Flutter 页面正常渲染六场景卡片通过
场景一:verify 验签成功,返回业务 JSON通过
场景二:decrypt 解密成功,返回业务 JSON通过
场景三:verifyAndDecrypt 链式调用成功,返回最终 JSON通过
场景四:篡改签名验签失败,按预期抛出异常通过
场景五:错误证书验签失败,按预期抛出异常通过
场景六:错误证书解密失败,按预期抛出异常通过
全程无需申请任何敏感权限通过

以下是操作的视屏,可以参考一下:

Example 启动授权


七、工作原理

整个调用链路如下:

Dart: SignedJson.verify(encoded)
  → SignedJsonUtil.internalVerify(cert, encoded)
    → useNativeSignedJson? → Platform.isAndroid
      → true (Android): NativeSignedJson.verify → MethodChannel('signed_json')
      → false (OHOS/iOS/Web): run(_verifyOnBackgroundThread, {cert, encoded})
        → compute(_verifyOnBackgroundThread, map)  // 后台 Isolate
          → JsonWebSignature.fromCompactSerialization(encoded)
          → JsonWebKey.fromJson(jsonDecode(cert))
          → JsonWebKeyStore()..addKey(jwk)
          → jws.getPayload(keyStore)  // ES512 验签
          → ZLibCodec().decoder.convert(payload.data)  // ZLib 解压
          → utf8.decode(unzipped)  // 返回 JSON 字符串
    → run(parseAndDecode, jsonString)  // jsonDecode
      → 返回 Map<String, dynamic>

Dart: SignedJson.decrypt(encoded)
  → SignedJsonUtil.internalDecrypt(cert, encoded)
    → useNativeSignedJson? → false (OHOS)
      → run(_decryptOnBackgroundThread, {cert, encoded})
        → JsonWebEncryption.fromCompactSerialization(encoded)
        → JsonWebKey.fromJson(jsonDecode(cert))
        → jwe.getPayload(keyStore)  // RSA1_5 解密 + A128CBC-HS256
        → ZLibCodec().decoder.convert(payload.data)
        → utf8.decode(unzipped)
    → run(parseAndDecode, jsonString)

Dart: SignedJson.verifyAndDecrypt(encoded)
  → internalDecrypt(certDecrypt, await internalVerify(certVerify, encoded))
    → 先验签外层 JWS → 得到内层 JWE 串
    → 再解密内层 JWE → 得到最终 JSON 字符串
  → run(parseAndDecode, result)

ArkTS: SignedJsonPlugin.onMethodCall (声明式桩)
  → 正常不会到达(OHOS 走纯 Dart 路径)
  → 若被调用: result.notImplemented()

Dart 侧的验签与解密全部为纯 Dart 实现,通过 jose 库完成 ES512 验签和 RSA1_5 解密,通过 ZLibCodec 完成解压。useNativeSignedJson 的判断条件是 Platform.isAndroid——只有 Android 平台走原生通道,鸿蒙、iOS、Web 均走纯 Dart 路径。

鸿蒙技术点:useNativeSignedJson 平台分流机制
base_signed_json.dart 中的 bool get useNativeSignedJson => Platform.isAndroid 是平台分流的核心。这一判断在编译时即可确定——鸿蒙环境下 Platform.isAndroid 为 false,因此 internalVerify 和 internalDecrypt 不会调用 NativeSignedJson.verify/decrypt(不经过 MethodChannel('signed_json')),而是直接走纯 Dart 路径:将参数封装为 ComputerArgs 的 JSON Map,通过 compute() 传递到后台 Isolate,在 Isolate 中执行 jose 库的验签/解密 + ZLibCodec 解压。这一机制保证了鸿蒙平台上的加解密行为与 iOS、Web 完全一致。

代码逐段分析:ComputerArgs Isolate 参数序列化
ComputerArgs 是一个简单的数据类,包含 cert(密钥 JWK 字符串)和 encoded(JWS/JWE 签名串)两个字段。compute() 方法要求参数和返回值都是可序列化的(因为需要跨 Isolate 传递)。ComputerArgs.toJson() 将对象转为 Map<String, dynamic>,在主 Isolate 序列化后传递到后台 Isolate,后台 Isolate 中通过 ComputerArgs.fromJson 反序列化重建对象。这一序列化-反序列化过程由 Dart VM 的消息传递机制保证,在鸿蒙上由引擎层实现,行为与 Android/iOS 一致。

鸿蒙侧插件实现(ArkTS)核心代码:

import {
  FlutterPlugin, FlutterPluginBinding, MethodCall,
  MethodCallHandler, MethodChannel, MethodResult,
} from '@ohos/flutter_ohos';

export default class SignedJsonPlugin implements FlutterPlugin, MethodCallHandler {
  private channel: MethodChannel | null = null;
  private static readonly TAG: string = 'SignedJsonPlugin';

  getUniqueClassName(): string {
    return 'SignedJsonPlugin';
  }

  onAttachedToEngine(binding: FlutterPluginBinding): void {
    this.channel = new MethodChannel(binding.getBinaryMessenger(), 'signed_json');
    this.channel.setMethodCallHandler(this);
    console.info(SignedJsonPlugin.TAG, 'Attached to engine (OHOS 使用纯 Dart 路径)');
  }

  onDetachedFromEngine(binding: FlutterPluginBinding): void {
    if (this.channel !== null) {
      this.channel.setMethodCallHandler(null);
      this.channel = null;
    }
    console.info(SignedJsonPlugin.TAG, 'Detached from engine');
  }

  async onMethodCall(call: MethodCall, result: MethodResult): Promise<void> {
    // 防御性处理:OHOS 实际走纯 Dart 路径,正常不会到达此处
    try {
      switch (call.method) {
        case 'verify':
        case 'verifyAndDecrypt':
        case 'decrypt':
          result.notImplemented();
          break;
        default:
          result.notImplemented();
          break;
      }
    } catch (e) {
      result.error('UNHANDLED_EXCEPTION', `处理 ${call.method} 时发生异常: ${e}`, null);
    }
  }
}

说明:SignedJsonPlugin 是一个声明式桩实现。鸿蒙平台上 useNativeSignedJson 为 false,Dart 层不会经由 MethodChannel('signed_json') 发起调用——加解密全部在纯 Dart 路径完成。原生插件的存在是为了满足 Flutter 插件注册机制的要求(pubspec.yaml 中声明了 ohos: pluginClass: SignedJsonPlugin),保证注册表完整与通道契约对齐。即使被调用,也统一返回 notImplemented,交由纯 Dart 兜底,绝不抛出未处理异常。

鸿蒙技术点:GeneratedPluginRegistrant 自动生成机制
GeneratedPluginRegistrant.ets 由 Flutter 工具链根据 pubspec.yaml 中的 ohos 平台配置自动生成。当 flutter pub get 解析到 signed_json 依赖声明了 ohos: pluginClass: SignedJsonPlugin 时,工具会在宿主工程的 example/ohos/entry/src/main/ets/plugins/ 下生成注册代码,将 new SignedJsonPlugin() 添加到 FlutterEngine 的插件列表。EntryAbility.configureFlutterEngine 中一行 GeneratedPluginRegistrant.registerWith(flutterEngine) 即完成插件注册。这一机制是 Flutter 插件生态在鸿蒙上无缝衔接的关键。

八、常见问题

Q1:verify 和 decrypt 有什么区别?

verify 用于验签 JWS(JSON Web Signature)——数据用私钥签名,客户端用公钥验签确认数据完整性和来源真实性。验签不需要私钥,只需公钥。decrypt 用于解密 JWE(JSON Web Encryption)——数据用公钥加密,客户端用私钥解密获取原文。解密需要私钥。两者可独立使用,也可通过 verifyAndDecrypt 链式组合——先验签外层 JWS 确保完整性,再解密内层 JWE 获取机密内容。

Q2:验签失败抛出什么异常?

验签失败时,jose 库的 jws.getPayload(keyStore) 方法会抛出异常——签名不匹配时抛出 JoseException,JWS 格式错误时抛出 FormatException,ZLib 解压失败时抛出 ZLibDecoderException。建议在业务层用 try/catch 捕获所有异常,统一视为"数据被篡改或格式错误"处理。

Q3:真机安装 demo 时提示"此应用暂不支持在当前设备安装"?

这是宿主工程的 compatibleSdkVersion 高于真机 API 版本导致的安装校验失败,与插件无关。将 build-profile.json5 中的 compatibleSdkVersion 调整为不高于真机 API 的版本(如 5.1.0(18),注意保留带括号的旧格式)即可。本文配套仓库的 example 已用此配置在 OpenHarmony-6.1.1.120(API 24)真机上安装实测通过。

Q4:如何生成验签和解密密钥?

使用 sign_config 工具或 mkjwk.org 在线生成。验签密钥:EC 类型,Key Size 选择 P-521,Key Use 选择 Signature,Algorithm 选择 ES512,取公钥部分(verify.jwk)。解密密钥:RSA 类型,Key Size 选择 2048 及以上,Algorithm 选择 RSA1_5 或 PS512,取私钥部分(decrypt.jwk)。公钥可安全地内嵌在客户端代码中,私钥应妥善保管在服务端。

Q5:为什么 OHOS 平台走纯 Dart 路径而非原生实现?

base_signed_json.dart 中 useNativeSignedJson 的判断条件是 Platform.isAndroid——只有 Android 平台走原生实现(因为 Android 上 jose 库的某些依赖在旧版本可能有兼容性问题,原库选择用 Java 原生实现替代)。鸿蒙、iOS、Web 上的 jose 库工作正常,因此走纯 Dart 路径。这保证了加解密逻辑在鸿蒙上与 iOS、Web 完全一致,且零原生依赖、零权限。

Q6:compute 后台 Isolate 在鸿蒙上可用吗?

可用。compute() 是 Flutter Foundation 层的 Isolate 并发工具,在鸿蒙平台上由 Flutter Engine 的 C++ 层完整实现 Isolate 创建、消息传递与销毁机制。jose 库的 ES512 验签和 RSA 解密在后台 Isolate 中执行,不阻塞 UI 线程。序列化-反序列化过程由 Dart VM 的消息传递机制保证,行为与 Android/iOS 一致。

九、结语

回顾一下:在 pubspec.yaml 中以 git TAG 引入 signed_json,构造 SignedJson(verifyCert, decryptionCert: decryptCert) 实例,调用 verify / decrypt / verifyAndDecrypt 三个方法即可在鸿蒙 App 内完成 JWS 验签、JWE 解密与链式调用。加解密全部走纯 Dart 路径(jose 库 + ZLibCodec),通过 compute() 在后台 Isolate 执行,不阻塞 UI 线程。同一份 Dart 代码在 Android、iOS、鸿蒙、Web 四端行为一致——Android 走原生实现,其余三端走纯 Dart 路径。签名被篡改任何一个字符都会导致验签失败并抛出异常,确保数据完整性。已在 OpenHarmony-6.1.1.120(API 24)真机完整实测,验签、解密、链式调用、篡改检测、错误证书检测六个场景全部通过。

使用中发现任何问题,欢迎到配套仓库提 Issue,也欢迎发 PR 共建。

相关链接

欢迎加入 CPF-Flutter 鸿蒙社区,社区入口、环境搭建指南和本文相关链接统一放在这里:


附:signed_json 核心能力对照表

能力维度实现方式鸿蒙表现跨平台一致性
JWS 验签jose 库 ES512 + ZLibCodec 解压纯 Dart 路径,一致Android 原生,iOS/鸿蒙/Web 纯 Dart
JWE 解密jose 库 RSA1_5 + A128CBC-HS256 + ZLibCodec纯 Dart 路径,一致Android 原生,iOS/鸿蒙/Web 纯 Dart
链式调用先验签 JWS 得 JWE,再解密 JWE 得 JSON纯 Dart 路径,一致四端完全一致
篡改检测jose 库签名验证,不匹配抛异常检测正常,抛异常四端完全一致
错误证书检测公钥/私钥不匹配,验签/解密失败检测正常,抛异常四端完全一致
后台计算compute() Isolate 并发引擎层支持,一致VM 端一致
参数序列化ComputerArgs.toJson/fromJson消息传递正常VM 端一致
平台分流Platform.isAndroid 判断false,走纯 DartiOS/鸿蒙/Web 一致
原生桩MethodChannel('signed_json')声明式,notImplemented通道契约对齐各平台
权限要求无无敏感权限四端均无
Logo

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

更多推荐