现在做 Flutter 的鸿蒙化适配,最怕遇到那种带着原生依赖的三方库。手头这个rsa_id_number就是典型:它把身份证号码校验、脱敏和 RSA 加密揉在一个包里,纯 Dart 部分能跑,但底层加密、平台通道、密钥托管全都得在鸿蒙上重新接一遍。这篇文章就是我这次适配的全过程笔记,从库的原理拆解讲到鸿蒙侧一步步改造,最后把安全治理和踩坑清单也一并整理出来,给准备把 Flutter 应用迁到鸿蒙生态的团队一个可以直接抄作业的参考。
这个库解决的核心问题很明确:把用户输入的身份证号码当作一项“身份标识资产”来管控,而不是普通字符串。它做了三件事:第一层是合法性校验,用国标的加权因子算法识别假号码;第二层是脱敏显示,避免界面和日志里露出完整号码;第三层是 RSA 加密,让敏感数据在存储和传输时不以明文出现。如果你是做实名认证、用户信息登记、金融风控类 App 的 Flutter 开发者,并且已经在评估鸿蒙适配的改造量,这篇内容值得看完。
1. 先拆开 rsa_id_number,看看它到底做了什么
1.1 库的定位:不是普通工具类,而是敏感数据的治理入口
很多人会低估这个库。乍一看它就是一个validate()加encrypt()的工具类,但实际上它承担了“身份标识资产”全链路治理的职责。身份证号一旦泄露,不只是隐私问题,还会被拿去注册、借贷、精准诈骗,所以成熟的工程不会只在校验函数里用一下就丢,而是会把“校验 - 脱敏 - 加密 - 传输 - 存储”这条链路统一收口到一个模块里面。
rsa_id_number比较聪明的地方,是它把校验规则和加密逻辑打包在一起,对外暴露的接口保持简单,内部却已经处理好了边界情况:15 位老号码、18 位新号码、末尾 X 的大小写、出生日期的合法范围、校验码的加权计算,都会一次性处理掉。适配鸿蒙时最忌讳的事情,就是只把validate()函数复制过去,忽略它背靠的那一套加密和脱敏能力,那样等于把一个治理工具用成了普通校验函数。
另外要注意,这个库依赖的底层能力分两层。上层是纯 Dart 写的校验和脱敏逻辑,几乎零成本就能迁移到鸿蒙;下层是 RSA 密钥生成、加解密和平台通道,这部分依赖所在系统的安全能力。鸿蒙原生侧提供了自己的加密框架,所以下层必须重写,这也是整个适配工作的主要工程量。
1.2 身份证号码校验的核心算法,先弄懂再加代码
身份证号码的校验逻辑网上到处都是,但真让你闭着眼写一遍,还是容易出错。这里用最简单的话把原理说清楚:18 位号码由 6 位地区码、8 位出生日期、3 位顺序码、1 位校验码组成。校验码不是乱编的,而是用前面的 17 位数字算出来的,用来防止录入错误和伪造序号。
计算过程分三步。第一步给前 17 位数字分配一组加权因子,因为每一位的权重不一样,从第 1 位到第 17 位分别是7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2。第二步把每一位数字和对应的加权因子相乘,再把 17 个乘积全部相加。第三步用总和除以 11 取余数,得到一个 0 到 10 之间的数,然后查校验码映射表得到最终校验码,映射关系是1 0 X 9 8 7 6 5 4 3 2,意思就是余数为 2 的时候校验码是 X。
有人可能会问,为什么要用 11 做模而不是 10?因为 11 是质数,和 10 以内的数字组合能产生更均匀的校验分布,错一位数字或者颠倒两位数字都更容易被查出来。这也是为什么不能只用正则表达式校验身份证号,正则只能管格式,管不了数学上的真假。
rsa_id_number的核心 Dart 实现大概是这个思路:
const List<int> _weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; const List<String> _checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']; String? validateIdNumber(String input) { if (input == null || input.isEmpty) return '号码不能为空'; final value = input.trim().toUpperCase(); if (!RegExp(r'^\d{17}[\dX]$').hasMatch(value)) return '号码格式不正确'; final digits = value.substring(0, 17).split('').map(int.parse).toList(); var sum = 0; for (var i = 0; i < 17; i++) { sum += digits[i] * _weights[i]; } final mod = sum % 11; final expected = _checkCodes[mod]; if (value[17] != expected) return '校验码不正确,应为 $expected'; return null; }注意末尾 X 的处理,这里做了toUpperCase(),因为真实使用场景里用户经常输入小写的 x,不做统一转换就会出现明明号码合法却被判非法的情况。这个细节在我踩坑记录里排得上前几名。
1.3 “RSA 治理”到底在治理什么
标题里的“精密 RSA 治理实战”不是营销话术。身份证号这种长度在 18 位左右的敏感字符串,用 RSA 加密在工程上存在两个容易被忽视的点:第一是 RSA 2048 能加密的明文长度有限,按 PKCS1 填充算大概只能处理 245 字节,如果号码拼接了其他信息一起加密,就会超出长度限制;第二是填充方式必须选 OAEP 而不是 PKCS1,OAEP 带随机盐,同样的明文每次加密结果不同,能有效防止字典攻击。
更关键的是密钥托管。很多团队在 Dart 层用本地生成的 RSA 私钥做加密,这个做法在互联网应用里是反模式。客户端持有私钥等于把密钥送到攻击者嘴边,脱壳、抓包、内存 dump 都有可能拿到。正确做法是客户端只保存服务端下发的公钥,私钥永远留在服务端。如果业务要求离线验签,再结合系统级安全能力把私钥锁进硬件安全区。
这里我按社区常见设计补全一下rsa_id_number的加密姿势:对外输入一个身份证号明文,内部先做校验和脱敏,然后调用 RSA 加密得到 Base64 密文,业务方拿密文去落库或上报。密钥来源支持两种,一种是开发者手动注入公钥,另一种是从服务端拉取公钥后缓存到本地的安全存储里。鸿蒙适配时,我强烈建议把第二种做成默认方案,因为鸿蒙系统本身提供了成熟的安全存储能力,比自己在 Flutter 侧写文件缓存靠谱得多。
2. 鸿蒙侧适配前的环境准备与兼容性摸底
2.1 鸿蒙 Flutter 生态现状:能跑,但“能跑”不等于“能直接用”
鸿蒙的 Flutter 适配方案已经不是纸面概念,当前主流的做法是使用鸿蒙生态提供的 Flutter SDK 分支配合 DevEco Studio 构建原生壳工程。对于纯 Dart 代码,迁移成本很低,Dart 虚拟机在鸿蒙上跑得挺稳;但对于依赖MethodChannel和原生加密库的三方库,就没那么省心了。
rsa_id_number这种库在鸿蒙上大概率会碰到三类问题。第一类,插件注册配置里没有ohos平台的声明,Flutter 工具链不会自动把原生代码编进鸿蒙工程。第二类,原生侧加密实现用的是 Android 的Cipher类或者 iOS 的SecKey,这套 API 在鸿蒙上不存在,必须换成鸿蒙的加密框架。第三类,依赖的纯 Dart 库如果用了dart:ffi调用系统 C 库,还得检查那些 C 库在鸿蒙上是否提供同名符号。
所以我建议动手之前先做一次“兼容性评估清单”,把rsa_id_number的依赖树拉出来,分三类打标:纯 Dart 实现、平台通道实现、FFI 实现。纯 Dart 部分直接搬,平台通道部分重写原生插件,FFI 部分单独验证。实测下来,rsa_id_number的校验和脱敏属于第一类,RSA 加密属于第二类,工作量主体落在第二类上。
2.2 环境准备的具体步骤
先说明一下我这次适配用的环境基线,不一定是最新版,但都是当前可用且稳定的搭配:鸿蒙 Flutter SDK 用社区维护的 ohos 分支,DevEco Studio 用来编译鸿蒙原生壳,pubspec.yaml里声明最低支持的鸿蒙 API 版本。不同版本的具体号段会变,重点看思路。
环境配置按下面几步走:
- 安装 DevEco Studio,启动后配置鸿蒙 SDK 路径,确认能创建一个空壳工程并在模拟器跑起来。
- 拉取鸿蒙 Flutter SDK,把它配置成 Flutter 的可用分支,运行
flutter doctor确认鸿蒙设备被识别。 - 创建一个测试工程,
pubspec.yaml里加入鸿蒙平台的插件声明,跑通一个最简的MethodChannel调用,验证原生到 Dart 的通道是通的。 - 把
rsa_id_number源码目录拷贝进工程,先只保留纯 Dart 的校验和脱敏部分,跑通单元测试,确认这部分不受鸿蒙影响。
第 3 步是最值得花时间的。很多人一上来就直接把整个库塞进工程,结果原生插件都还没注册成功,报错都不好定位。先用一个三行代码的最小通道验证环境,后面排查问题时能省一半时间。
2.3 鸿蒙原生插件机制和 Android 的差异点
鸿蒙 Flutter 插件的工程结构里会多出一个ohos目录,里面是完整的鸿蒙模块。原生代码入口类需要继承 Flutter 适配框架里的插件接口,并且用MethodChannel接收 Dart 侧调用。和 Android 最大的差异在于注册机制:Android 用GeneratedPluginRegistrant自动扫描插件,鸿蒙这边需要在模块的初始化逻辑里手动完成注册,因此pubspec.yaml里pluginClass的声明必须和原生入口类的名字严格一致,大小写都不能错。
另外一点,鸿蒙原生侧访问加密框架的方式也和 Android 完全不同。Android 开发者习惯直接写Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"),鸿蒙上却要经过统一的加密框架来创建算法实例,并且很多接口是异步回调风格。这一点直接影响了后面原生 RSA 模块的代码风格,我在第三章展开讲具体写法。
3. 核心适配实操:从 Dart 到鸿蒙原生的一步步改造
3.1 第一步:在 pubspec.yaml 中声明 ohos 插件平台
要让 Flutter 工具链识别鸿蒙平台,需要在插件的pubspec.yaml中声明:
flutter: plugin: platforms: ohos: pluginClass: RsaIdNumberPlugin dartPluginClass: RsaIdNumberOhos这段声明的作用是告诉构建工具:这个库的鸿蒙原生入口类叫RsaIdNumberPlugin,Dart 侧还有一个专门提供给鸿蒙平台的封装类叫RsaIdNumberOhos。很多适配失败案例卡在这一步,因为漏写dartPluginClass,导致运行时直接走默认通道,找不到对应实现。
ohos目录下的鸿蒙模块结构大概是这样的:
rsa_id_number/ ├── lib/ ├── android/ ├── ios/ ├── ohos/ │ ├── entry/src/main/ets/ │ │ ├── ets/ │ │ │ └── RsaIdNumberPlugin.ets │ │ └── module.json5 │ └── build-profile.json5 └── pubspec.yamlmodule.json5里要确认 module 名称和 Flutter 壳工程预期的一致,否则原生代码编译过了,运行时报错找不到模块。
3.2 第二步:用 ArkTS 重写原生 RSA 加密模块
这是整个适配工程里最核心的部分。我的做法是在 ArkTS 侧创建一个RsaIdNumberPlugin类,实现 Flutter 适配框架要求的生命周期,在通道回调里接收三个方法:validate、mask、encrypt。
encrypt方法的实现逻辑如下:
- 从 Dart 侧接收 Base64 编码的公钥。
- 调用鸿蒙加密框架创建 RSA 密钥生成器或者直接构建公钥对象。
- 选择
RSA_ECB_OAEP_SHA256算法创建加密器。 - 对明文进行加密,把密文转成 Base64 字符串返回给 Dart 侧。
ArkTS 侧的骨架代码思路是这样的:
import cryptoFramework from '@ohos.security.cryptoFramework'; function doEncrypt(plaintext: string, pubKeyBase64: string): Promise<string> { const pubKey = cryptoFramework.createPubKey('RSA2048', pubKeyBase64); const cipher = cryptoFramework.createCipher('RSA_ECB_OAEP_SHA256'); return cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, pubKey) .then(() => cipher.update({ data: new Uint8Array(...) })) .then(() => cipher.doFinal()) .then(() => Base64Helper.encode(cipher.output)); }这里有几个关键点必须说明:第一,实际代码里update和doFinal的调用顺序要按照 SDK 版本调整,不同 API 版本对update的返回值处理方式略有差异;第二,createCipher的算法名不要写错,写错一个字直接抛异常,而且异常信息特别不好查;第三,所有异步操作要处理好错误链,公钥格式不合法、明文过长、填充方式不匹配都会导致加密失败,错误信息需要返回给 Dart 侧做成可读提示。
为什么公钥要从 Dart 侧传而不是在原生侧写死?因为公钥是会轮换的。写死在原生代码里,每次换密钥都要发版本;传参进来就能走服务端下发 + 缓存的机制,密钥失效时还能触发重新拉取的流程。
3.3 第三步:Dart 侧改造与调用链收口
原生侧的通道打通后,Dart 侧还要做一层收口。rsa_id_number原来的调用方式是直接同步校验、同步脱敏、同步加密,鸿蒙适配后encrypt变成了异步方法,因为原生通道调用本身就是异步的。这个接口变化会影响所有调用方,所以我建议在 Dart 侧加一个兼容层,旧接口保留,新接口用async版本,避免上层业务大改。
完整调用链应该是这样的:
final result = await RsaIdNumber.ohos() .validate(inputNumber); // 第一步:校验 if (result.hasError) return; final masked = RsaIdNumber.mask(inputNumber); // 第二步:脱敏展示 final encrypted = await RsaIdNumber.ohos().encrypt(inputNumber); // 第三步:加密 sendToServer(encrypted);这里重点强调一下调用顺序:校验、脱敏、加密这三个动作的顺序不能乱。先校验可以拦截掉大部分非法输入,避免垃圾数据进入加密流程浪费性能;脱敏只用于界面展示和日志输出,绝不参与加密,因为脱敏后的数据是不可逆的,加密它没有任何意义;真正要加密的一定是完整明文。
还有一个很容易被忽视的细节,mask函数也应该在鸿蒙侧实现,而不是复用 Dart 侧的实现。原因是为了保证日志脱敏策略全局统一。如果 Dart 侧脱敏和原生侧脱敏各写一套规则,很容易出现版本不一致,比如某次紧急合代码,原生侧漏掉了中间四位打星号的逻辑,身份证号就跟着日志泄出去了。统一收到一个模块里,审查时只盯一个文件就行。
4. 常见问题与排查技巧实录
4.1 典型问题速查表
以下几类问题是适配过程中出现频率最高的,整理成表格方便直接对照:
| 现象 | 可能原因 | 解决方式 |
|---|---|---|
| 运行时报“通道找不到实现” | pubspec.yaml的 ohos 声明缺失或 pluginClass 拼写错误 | 检查声明块,确认 pluginClass 和原生入口类完全一致 |
| 身份证号校验时合法号码被判非法 | 未处理小写 x,或校验码表索引错误 | 在入口处统一toUpperCase(),用测试用例覆盖校验位为 X 的号码 |
| RSA 加密返回数据过长 | 明文超出当前填充方式限制,或误把 Base64 密文再做了 Base64 | 确认待加密内容长度,必要时拆段加密,密文只编码一次 |
| ArkTS 加密 API 抛异常但日志里看不见原因 | 异步 Promise 的 reject 没有透传 | 在原生侧 catch 所有异常并转换错误码返回 Dart |
| 鸿蒙模拟器上首次调用加密特别慢 | 系统密钥库初始化延迟 | 冷启动时做一次预热调用,后续性能恢复正常 |
| 插件在鸿蒙手机上偶发注册失败 | 原生模块初始化顺序问题 | 检查module.json5模块声明,确保在 Flutter 引擎 attach 前完成注册 |
4.2 排查思路:先定位层级,再动手改
遇到问题我习惯先判断是哪个层面的问题:是 Dart 纯逻辑层、是通道层、还是原生加密层。判断方法很简单,在 Dart 侧直接调用mask函数,如果脱敏正常,说明 Dart 逻辑没问题;调用encrypt如果失败,先看有没有走到原生通道的入口日志,走到入口就是原生逻辑问题,没走到入口就是通道注册问题。
通道层的问题最隐蔽。鸿蒙上 MethodChannel 的通道名和 Android 一样,都是rsa_id_number,但很多开发者照搬 Android 的注册写法,忘了鸿蒙原生侧注册通道的时机比 Android 更严格。有一个笨但有效的办法:在原生通道的setMethodCallHandler入口打好日志,任何调用进来先打一条,这样就能快速确认 Dart 侧发出的调用到底有没有到达原生侧。
4.3 三个踩坑最深的性能与兼容细节
第一个坑是 RSA 分段加密。身份证号本身不长,但有些业务会把“号码 + 姓名 + 手机号”拼成一个 JSON 再加密,一下子超过 RSA 单次加密上限。鸿蒙加密框架不会帮你自动分段,超长直接报错。如果确认有这个需求,就得在 Dart 侧分块调用,每块最大长度按填充方式计算,PKCS1 是 245 字节,OAEP 是 190 字节左右。不过我更建议从业务上禁止这种拼接,一个字段一个字段分开加密反而更好解密和维护。
第二个坑是加密结果的编码转换。原生侧doFinal拿到的输出是字节数组,返回给 Dart 前必须转成 Base64。有同事经验不足,直接转成了 UTF-8 字符串,结果密文变乱码,还以为是加密算法选错了,折腾了半天。编码问题最消耗排查时间,建议把“字节数组到 Base64”这一步封装成公共方法,所有加解密出口统一走它。
第三个坑是密钥轮换后老数据解不开。上线初期用公钥 A 加密,两个月后轮换公钥 B,服务端如果用 B 去解密以前 A 加密的数据,全部解不开。这个不是代码 bug,而是架构问题。我建议密文传输时附带一个密钥版本字段,比如 JSON 里加keyVersion: 1,服务端根据版本号选择对应私钥,才算把 RSA 治理闭环。
5. 身份标识资产的后续安全加固与测试验证
5.1 日志与崩溃采集端的二次加固
适配鸿蒙之后,rsa_id_number本身可以正常工作了,但还有一个大问题:崩溃日志采集库也会被带入鸿蒙工程。如果 Flutter 侧某个未捕获异常带着身份证号明文一起上报,那前面的加密全部白费。我见过最典型的场景是表单校验失败后,开发者为了排查直接把整个请求体print出来,请求体里装着明文身份证号,崩溃上报平台一收一个准。
这里给一个可落地的加固清单:
- 所有 Dart 侧的
print、debugPrint在发布版统一替换为带过滤的日志函数,对idCard、identity等字段强制走脱敏方法后再输出。 - 原生侧的 ArkTS 代码里不打印任何请求参数,只打印方法名和耗时。
- 网络库的日志拦截器默认关闭 body 输出,需要排查时再局部打开,并限定在 debug 包。
- 崩溃采集 SDK 里配置字段过滤规则,把可能包含敏感信息的关键字替换成
***。
这条链路加固完,才算真正做到“掌控身份标识资产”,而不是只在业务代码里加密。
5.2 自动化校验的完整测试清单
适配完成后,我整理了一份校验测试用例,覆盖四类场景:合法号码、格式非法但校验码正确的号码、校验位为 X 的号码、15 位老号码。合法的边界组合要重点测出生日期的平闰年,校验码算法只管数字不管日期,但业务层多半会叠加日期合法性判断,rsa_id_number如果有这个参数要单独验证。
脱敏规则测试也不难,但容易漏边界:号码长度为 15 和 18 时脱敏后的长度可能不同;号码中间四位是星号还是后四位,按产品设计而定;脱敏后的字符串坚决不能和原号码存在可逆关系。这些用例最好在鸿蒙设备上用自动化脚本跑一遍,不要只靠模拟器,因为原生加密通道在真实设备上的行为更接近线上。
5.3 这层适配做完,后续还能怎么扩展
适配完成并不是终点。鸿蒙系统级的安全能力比 Flutter 侧自研方案更强,后续可以把密钥托管进一步下沉到系统硬件安全区,实现私钥不出硬件、加解密操作在安全区内完成。rsa_id_number这类库在鸿蒙上站稳之后,还能把整个能力扩展成一个小型敏感数据治理 SDK,把银行卡号、手机号、邮箱的校验和加密全部收进来,统一出口、统一审计。
我个人在实际操作中的体会是:这次适配最花时间的不是通道桥接,也不是 RSA 算法,而是想清楚“密钥放哪、明文怎么清、日志怎么过滤”这三件事。技术方案网上都有,但安全治理的视角需要自己搭起来。如果团队里正准备做同类适配,我的建议是先花一天时间做一个最小工程验证通道,再花半天把脱敏规则定死,最后才开始搬加密逻辑,这个顺序能避开大部分返工。