rns_id_number 鸿蒙化适配:身份证校验与 RSA 加密实战 1. 什么是 rsa_id_number为什么要做鸿蒙化适配1.1 身份证校验场景在鸿蒙环境下的真实需求在移动应用里身份证号校验和加密保护是一个很常见但又特别容易做砸的功能。很多业务场景都需要在客户端完成身份证号的格式验证比如实名认证、金融开户、健康登记、物流实名签收甚至在员工考勤和访客管理里都会用到。光是“格式正确”这一关就卡掉不少开发者的手工正则方案。更麻烦的是身份证号属于高度敏感的个人身份资产APP 一旦涉及传输和存储就必须考虑加密保护。很多团队会在客户端用 RSA 公钥加密后再提交给服务端避免明文落库和被抓包直接读到。rns_id_number 这个 Flutter/Dart 三方库天然就做了两件事一是完成了中国大陆身份证号码的完整校验二是提供了基于 RSA 的加密能力。这两件事在纯 Flutter 环境里跑起来没有问题但放到鸿蒙环境里就不一定了。这里的“鸿蒙环境”指的是 HarmonyOS NEXT 这类不再兼容 Android APK、基于 OpenHarmony 技术底座、以 ArkTS 和系统原生 API 为核心的应用生态。Flutter 虽然已经能通过鸿蒙化的 Flutter SDK 跑起来但底层能力边界和 Android/iOS 并不完全一致。三方库如果依赖了某些在 Android 上常见的原生能力、系统服务或加密桩件就可能出现编译不通过、运行时找不到符号、或者加解密结果不一致的问题。我接手这个适配工作时最直接的感受就是这类“小而关键”的库坑往往不在业务逻辑本身而在依赖链和底层能力映射上。你花在环境配置和依赖排查上的时间可能比改 Dart 代码的时间多得多。所以在开始动手之前先别急着复制粘贴代码一定要把库的底层依赖、加密实现方式和运行机制彻底弄清楚否则后面会反复返工。1.2 鸿蒙化适配的边界Flutter 在鸿蒙上到底能做什么需要先明确一点鸿蒙系统确实支持 Flutter 应用运行但“支持”是有边界的。官方维护的 Flutter 鸿蒙化 SDK 通过一套定制的引擎和插件注册机制让 Dart 代码可以跑在鸿蒙的框架之上。对于纯 Dart 实现的库理论上可以直接复用但对于涉及原生能力如网络、文件、加密、生物识别等的功能就需要通过鸿蒙侧的插件提供能力映射。rns_id_number 如果只做纯 Dart 的正则校验和校验码计算鸿蒙化适配几乎零成本。但它如果用了 Dart 层的纯算法库来实现 RSA那就需要进一步确认这个纯算法库是否能在鸿蒙环境的 Flutter 引擎里正常执行。如果库里并没有依赖任何原生插件只是用 Dart 实现了 RSA 加解密那问题就主要出在“能不能编译通过”和“运行时和大数字运算是否正常”上。如果库内部依赖了某个原生加密中间层那就要做好“替换加密实现”的改造准备。我在做适配前先把 rns_id_number 的源码和 pubspec 依赖全文过了一遍。比较幸运的是它属于“纯 Dart 少量通用依赖”的典型结构。但即便是这样我也没有直接开始编译而是先做了完整的依赖树分析。这一步看似多余实际上能帮你省掉大量的试错时间。鸿蒙化适配的大忌就是“猜”。你猜某个库能跑结果编译报错你猜某个依赖被支持结果运行时直接闪退。精准地拆解依赖树比什么都重要。2. 适配前的依赖拆解与架构认知2.1 核心模块划分校验模块与加密模块rns_id_number 从功能角度看大致可以拆成四块基础信息解析比如从身份证号中提取出生日期、性别、地区码、校验码。格式与规则校验包括长度、字符类型、出生日期合法性、地区码合理性、校验码计算。伪号码识别对明显不合理的号码进行拦截比如 000000、123456 等。RSA 加密保护将身份证原文或摘要使用公钥加密配合服务端私钥解密。前三个模块完全依赖 Dart 层的逻辑不涉及任何系统能力。这类代码在鸿蒙化的 Flutter 引擎中理论上可以直接运行。需要重点考察的是第四个模块RSA 加密部分的底层实现到底依赖了什么。我在源码中看到的 RSA 加解密逻辑本质上走的是标准的 PKCS#1 填充和模幂运算。Dart 生态里实现这一类算法通常有两种路线一是使用 pointycastle 这类纯 Dart 加密库二是直接调用系统加密接口。rns_id_number 使用的是纯 Dart 方案这在鸿蒙化适配里是加分项因为不需要额外编写平台代码也不需要在鸿蒙侧重新实现密钥存储能力。但纯 Dart 方案的劣势也很明显大数运算效率完全取决于 Dart 引擎的执行效率加密大数据块时会明显慢于原生加密接口。对于一个身份证号加密场景来说数据量极小性能影响基本可以忽略。2.2 依赖项逐一确认能留的留不能留的换适配过程中我对 rns_id_number 的 pubspec.yaml 逐条做了确认collection / meta 这类纯 Dart 工具库鸿蒙化的 Flutter SDK 对它们支持良好无需修改。crypto / convert 这类纯 Dart 编码转换库通常也能直接编译只有涉及特定哈希实现时需要核对。pointycastle如果引入了纯 Dart 实现理论上兼容但要留意它内部是否使用了 dart:typed_data 里的某些 API这些 API 在鸿蒙引擎上基本可用但仍建议实际跑一遍单测。一开始我踩过一个预期外的坑项目里有一处代码使用了dart:io的Platform类来判断运行环境。这个类在鸿蒙环境的 Flutter 引擎里不是完全不可用而是有行为差异。如果你在适配时看到库里有“根据平台走不同分支”的代码建议把“鸿蒙”分支当成一个新的独立分支来加不要想当然地归到 Android 或 Linux 分支里。这里给一个可以复用的检查清单排查依赖时不要只看 pubspec.yaml 声明的顶层依赖还要检查传递依赖。建议用flutter pub deps --stylecompact输出完整依赖树再逐条对照鸿蒙 SDK 的兼容性说明。遇到不认识的包直接去源码里看它是否使用了 dart:ui / dart:io / dart:ffi 等平台相关能力。3. 鸿蒙化适配的整体设计思路3.1 明确适配目标既要功能可用也要结果一致适配不是“能编译过就算赢”而是要做到业务结果与原有实现完全一致。尤其是身份证校验和 RSA 加解密这两块任何细微偏差都可能导致线上问题。比如身份证校验码算错一位会导致用户被误判为“号码无效”RSA 加解密如果填充模式不一致会导致服务端解密失败或者相同的原文加密出完全不同的密文当然 RSA-OAEP 和 PKCS#1 都是随机填充密文不同是正常的但解密结果必须一致。所以在整体设计中我给自己定了几条硬指标原有 Dart 层校验逻辑保持原封不动只做必要的鸿蒙平台裁剪。RSA 加解密结果必须与 Android 端的既有实现互通即公钥加密后的密文可以被同一个服务端私钥解密。对身份证输入输出编码保持 UTF-8/ASCII 兼容避免中文环境下的隐蔽问题。单元测试必须覆盖边界情况比如 15 位旧号、18 位新号、含非法字符的输入、全 0 地址码等。3.2 技术路线保留纯 Dart 加密不引入原生加密插件我最初考虑过两条路线路线 A保留 rns_id_number 的纯 Dart 加密实现。优点是不用写平台代码维护成本低缺点是点对点调试困难大数运算性能受 Dart 引擎限制。路线 B在鸿蒙侧用系统加密框架重写加密模块通过 MethodChannel 或者鸿蒙的插件机制暴露给 Dart 调用。优点是性能和安全性更好能利用系统级密钥库缺点是工作量大且会把一个纯 Dart 库改造成双端异构库后续维护和版本同步会很麻烦。在身份证号加密这种小数据量场景下路线 B 的性能优势根本体现不出来反而会带来额外的工程复杂度。所以我最终选了路线 A只对必要的底层依赖做了鸿蒙化适配不额外引入原生加密插件。这里分享一个判断原则如果业务场景的数据量很小、对加密性能不敏感那么“纯 Dart 加密”就是最佳选择。你不需要为了“数字化治理”这种概念去迷信原生加密接口。鸿蒙系统加密框架再强对于 18 位数字的 RSA 公钥加密来说也不能带来任何用户可感知的差异。工程化追求的是恰当而不是越高越强。4. 实操过程从环境搭建到构建完成4.1 环境准备Flutter 鸿蒙化 SDK 配置鸿蒙化适配的第一步是搭好 Flutter 鸿蒙环境。由于整个工具链和标准 Flutter SDK 有差异不能直接拿普通 Flutter 命令来编译鸿蒙工程。我实际操作时的步骤大致如下拉取并切换到支持鸿蒙的 Flutter SDK 分支。配置鸿蒙开发环境变量比如 SDK 路径、工具链路径。在项目根目录执行flutter pub get确认依赖能正常解析。创建或者复用鸿蒙工程的entry模块把 Flutter 模块作为依赖接入。使用 hvigor 构建工具编译鸿蒙工程而不是直接使用 Gradle 或者 Xcode。这几个步骤里最容易出问题的是第 2 步和第 5 步。环境变量没配对构建工具连 SDK 都找不到hvigor 的版本和 Flutter 鸿蒙插件的版本不匹配会出现一堆莫名其妙的编译错误。建议你在开始之前先把构建工具链的版本矩阵查清楚再用一个最小的 Flutter Demo 工程验证整条链路是通的再引入 rns_id_number。不要一上来就把业务工程和适配工作混在一起否则环境问题和代码问题会互相干扰排查成本非常高。4.2 将 rns_id_number 接入鸿蒙 Flutter 工程工程环境通了之后引入 rns_id_number 其实和普通 Flutter 工程没有区别。在 pubspec.yaml 里声明依赖然后执行flutter pub get。但这里有一个容易被忽略的地方纯 Dart 库的传递依赖里如果存在某个插件声明了 Android/iOS 平台实现鸿蒙构建系统会尝试查找对应平台的注册文件。如果在鸿蒙目录下找不到同名插件的实现就有可能导致构建失败。遇到这种情况处理办法是分两步走第一步确认这个插件在当前业务代码里是否真的被用到第二步如果没用到就在依赖配置里把它裁剪掉或者在 Flutter 的 plugin registrant 配置里忽略对应平台。不要拖着一个无用的依赖到处“兼容”那只会无限放大鸿蒙化的风险面。我在适配 rns_id_number 时正好遇到一个传递依赖在鸿蒙端没有实现文件的情况。查阅文档后确认它只是某个工具包的可选支持库和身份证校验没有关系所以在鸿蒙工程的插件注册配置里去掉了对应条目构建立刻通过。4.3 身份证校验模块的鸿蒙侧验证身份证校验模块不涉及原生能力所以我优先对它做了鸿蒙环境下的单元测试验证。这个模块里的核心逻辑包括加权因子计算校验码比对出生日期范围判断地址码合法区间判断15 位转 18 位逻辑我在鸿蒙模拟器和真机上分别跑了一组测试用例确认结果与 Android 端完全一致。这部分几乎没有改动只是把dart:io的平台判断语句改成了更通用的条件判断。如果你看到的版本里也写了平台分支建议用“能力探测”代替“平台类型判断”因为鸿蒙环境很难被归入某个已有的平台枚举里。比如判断“是否支持某个 API”比判断“当前是不是某个平台”更稳妥。4.4 RSA 加解密模块的端到端验证RSA 加解密是这次适配的重头戏。我用了三组不同长度的 RSA 密钥对进行验证包括 1024、2048 和 4096 位。这里有一个必须注意的更新趋势很多服务端在密钥长度上已经强制要求 2048 位及以上1024 位在一些安全合规场景不再被接受。但 rns_id_number 本身只是按标准算法执行密钥长度由调用方传入。所以适配的关键不是改算法而是确认“1084”这种原有调用方式是否还有必要保留。实际操作中我采用了一个固定测试流程在鸿蒙 Flutter 环境内生成 RSA 密钥对如果库支持或者直接导入已有的 PEM 公钥。用公钥对指定身份证号原文进行加密。拿同一份密文在服务端或者另一个标准环境下用私钥解密。比对解密结果和原始输入是否一致。这个流程被我反复执行了多轮。前几轮出现的失败基本都是因为密钥格式和填充模式的搭配问题。点对点验证极其重要不能只校验“能加密”还必须校验“跨端能解密”。我见过太多只在本端自测通过、上线后服务端解密失败的案例。5. 关键细节校验算法与 RSA 参数在鸿蒙端的对齐5.1 身份证校验规则复盘不是简单的正则很多新手对身份证校验的理解就是“18 位 最后一位是 X”。实际上完整的校验规则要复杂得多。rns_id_number 的实现重点在于校验码的计算将前 17 位分别乘以不同的加权因子然后求和并取模 11再通过一张映射表得到校验码。这个计算逻辑虽然不复杂但出错率很高因为加权因子和映射表是两套不直观的数字数组。在做鸿蒙化适配时我特意把这段计算逻辑抽成独立函数并在鸿蒙环境下做了一组对照测试。一批号码在 Android 端计算出什么结果在鸿蒙端必须计算出什么结果不允许有任何误差。因为纯 Dart 代码在不同引擎下理论上应该一致但如果你依赖了某些平台相关的编码处理就有可能出现“在此端能跑、彼端跑出不同结果”的问题。尤其是在处理带 X 的尾号时大小写问题很容易被忽略。rns_id_number 的做法是统一转成大写我在适配时保留了这一行为并在文档里加了一行说明提醒接入方注意尾号 x 与 X 的兼容逻辑。5.2 RSA 填充模式与密钥格式对齐RSA 加解密最容易踩的坑就是密钥格式和填充模式不一致。rns_id_number 面向的服务端接口如果使用了公钥加密、私钥解密那么两端必须协商确定密钥是 DER 格式、PEM 格式还是 Base64 裸 Key填充是 PKCS#1 v1.5 还是 OAEP加密结果是 Base64 编码还是 Hex 编码。鸿蒙端的 Flutter 环境本身不限制这些约定但你在适配时如果引入任何参数转换就要特别注意转换后的字节序是否正确。比如 RSA 公钥的指数和模数在大端序和小端序之间存在完全不同的表示一旦把字节序搞反加密出的密文在服务端会直接解密失败而且报错信息非常隐晦。我的建议是把“密钥解析”这一层单独封装写一个独立的转换函数并在适配过程中增加一个“已知密文解密验证”的测试用例。也就是说你不光要验证“用你的公钥加密后能否被服务端解密”还要验证“服务端返回的已知密文在你的环境下能否被正确解析”。后一种验证能快速暴露密钥格式层面的不一致。5.3 编码问题和字符集边界身份证号本身由数字和 X 组成理论上不涉及中文编码。但在实际业务中用户输入可能携带不可见字符比如全角数字、前后空格、Unicode 零宽空格。rns_id_number 在 Dart 层做了 trim 和一些字符清理但我在鸿蒙环境测试时发现部分输入法生成的字符串会包含特殊 Unicode 字符导致长度校验误判。建议在接入层再加一道清洗逻辑先做 Unicode 归一化把全角数字转半角再去掉所有非字母数字字符最后再交给校验库处理。这道清洗逻辑与鸿蒙化本身没有直接关系但能显著降低用户侧录入差异引发的投诉。6. 问题排查与避坑实录6.1 常见问题速查我把这次适配过程中遇到的典型问题和排查思路整理成了一个速查表。这里面的问题不一定每个接入方都会遇到但一旦遇到你可以直接对号入座。问题现象可能原因排查思路编译时报找不到某个 Dart 库的实现传递依赖在鸿蒙端没有对应插件实现检查依赖树移除无用的平台相关依赖运行时提示某个 dart:io API 不可用dart:io在鸿蒙引擎上有行为差异用能力探测替代平台类型判断校验码结果与 Android 不一致身份证尾号大小写逻辑不一致统一转大写并添加单测RSA 加解密跨端失败填充模式或密钥格式不匹配做“已知密文解密验证”加密后输出结果非常长服务端拒绝密文编码方式不一致确认是 Base64 还是 Hex统一编码构建通过但启动时插件注册失败插件注册表缺少鸿蒙端配置检查鸿蒙工程的插件配置文件大量号码校验时性能慢Dart 大数运算效率问题可改成简洁的加权算法避免低效循环6.2 三个我最想提醒的坑第一个坑是“不要为了高级而高级”。我见过有人为了适配鸿蒙把 rns_id_number 里好好的纯 Dart 加密全部重写成原生加密。结果是工作量翻倍功能毫无提升后续升级三方库时还要重新适配。身份证号加密场景根本不需要高性能纯 Dart 完全够用。第二个坑是“不要在没跑通最小工程时就开始改代码”。我一开始就是因为环境变量漏配置导致编译报错误以为是 rns_id_number 本身的问题白白浪费了半天时间。后来重新搭了一个空的 Flutter 鸿蒙工程确认环境没问题才回来处理库的适配。这个顺序不要搞反。第三个坑是“忽略平台类型判断的死代码”。鸿蒙环境在很多库的作者眼里是不存在的所以你会看到类似“if (Platform.isAndroid) {} else if (Platform.isIOS) {} else {}” 的分支。在鸿蒙上这个 else 分支可能被意外执行。不要想当然地觉得“else 里是默认逻辑应该没问题”一定要逐段确认 else 分支做了什么。我在适配时就发现else 分支里有一段代码会把所有传入号码默认当成 15 位处理导致 18 位号码全部报错。这个 bug 在 Android 和 iOS 上永远不会暴露因为那两个平台都有明确分支只有鸿蒙才会走进 else 分支。6.3 如何设计一套可回归的测试数据集适配工作完成后非常重要的一步是留下可回归的测试数据集。我整理了三组数据第一组是正常合法号码涵盖不同出生年代和地区第二组是典型非法号码包括错误校验码、非法日期、非法地区码第三组是边界输入比如 15 位旧号、18 位全数字、最后一位为 X 的号码、全角数字输入、带空格输入。每组数据我都附加了预期结果包括校验是否通过、解析出的出生日期、性别、地区信息。这套数据集不仅用于鸿蒙端适配验证后续每次升级 rns_id_number 或者调整依赖版本时都可以用来做回归。工程化能力强的团队还会把这套数据集做成自动化测试脚本集成进 CI/CD一旦出现结果偏差马上报警。对于身份证号校验这种“出错一次就可能引发用户投诉”的功能回归测试不是可选项而是必选项。7. 一些个人体会整个 rns_id_number 鸿蒙化适配做下来我最深的感觉是适配工作本身并不难难的是想清楚“哪些东西不应该动”。身份证校验逻辑不需要动RSA 算法本身不需要动Dart 层的工程结构也不需要动。真正要动的是对鸿蒙平台特殊性的理解和适配。把这一点想透后面的一切都是执行问题。如果你也在做类似的三方库鸿蒙化适配建议从最小场景开始先让这个库在鸿蒙工程里能编译、能跑通一条最简单的校验流程然后再逐步扩展边界测试。不要一开始就追求“一步到位”那样既不利于定位问题也容易因为环境因素干扰判断。一个小技巧是适配过程中保留一份 Android 端的可运行工程两边同时跑同一批测试用例并直接对比输出。凡是两边结果不一致的地方就是你的适配清单里最需要关注的地方。这个方法帮我快速锁定了好几个隐蔽问题比埋头看代码高效得多。