开源鸿蒙平台 KMP 三方库「KMPAuth」适配全流程
本文记录
KMPAuth接入 OpenHarmony 的完整过程,覆盖上游库盘点、Compose 边界切分、真实认证引擎编译、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI / N-API 桥接、ArkTS 回注 HTTP 传输、ArkUI 真机页面、HAP 构建和真机验收。
KMPAuth是 Kotlin Multiplatform 生态中广泛使用的 Compose Multiplatform 认证库:Google / Apple / Facebook 登录、邮箱密码、匿名、手机号等凭据源,叠加 Firebase / Supabase 会话后端,UI 侧给出GoogleButtonUiContainer这类开箱即用按钮与rememberXxxSignInState状态。本次适配与社区早期路线(qrose / haze 的"零依赖内核提取"、KStore 的"存储引擎原样编译")一脉相承但有本质增量:认证引擎(KMPAuth门面、后端注册表、凭据模型、类型化异常)与 Firebase Auth REST 引擎被原样编译成 OpenHarmony KLIB,Compose UI 隔离进不参与 ohos 编译的源集;而认证库绕不开的 HTTP,以"ArkTS 回注传输"的方式解决——.so内不链 TLS,引擎挂起、请求冒泡给 ArkTS 系统网络栈执行、响应回灌继续,一个完整的注册 / 登录 / 匿名 / token 刷新 / 会话持久化闭环运行在真机上。
项目地址: AtomGit/oh-tpc/KMPAuth.git
上游仓库: mirzemehdi/KMPAuth
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
KMPAuth 为 Kotlin Multiplatform 应用回答"登录这一件事":凭据从哪来(provider:Google / Apple / Facebook / 邮箱 / 匿名 / 手机号)、会话交给谁管(backend:Firebase / Supabase / 自定义)、UI 怎么接(Compose 按钮容器 + 状态)。3.0 版本把架构切成了三层——kmpauth-core 定义 AuthProviderBackend 接口与进程级注册表,providers/* 只管凭据获取,backends/* 只管会话——KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、浏览器共享。OpenHarmony 应用同样需要这套认证能力,但上游构建(Kotlin 2.4.0 / Gradle 9.6.1 / AGP 9.2.1 / Compose 1.11)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明公共认证引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证会话引擎、REST 协议层、token 刷新链路、文件持久化、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.4.0 / Gradle 9.6.1 / AGP 9.2.1(com.android.kotlin.multiplatform.library)与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| 依赖矩阵 | 引擎依赖 kotlinx-coroutines、kotlinx-serialization-json,每个都要有 ohos 变体;GitLive firebase-kotlin-sdk 与 supabase-kt 没有 ohos 制品。 |
| Compose 边界 | core 与各 provider 的 commonMain 直接 import androidx.compose.*,社区 CMP 线与上游 Compose 1.11 / Kotlin 2.4 组合对不齐。 |
| HTTP 边界 | REST 引擎的平台接缝是"1 个 HTTP 传输 + 1 个会话存储";Kotlin/Native 侧没有现成免 TLS 负担的 HTTP 栈。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:认证引擎与 Firebase REST 会话后端的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI / N-API 桥接层。认证协议与错误映射完全由上游原版实现负责,ArkTS 只负责调用 Native、执行回注 HTTP、解析 JSON 和展示结果。
1.2 库提供的能力
上游能力按"ohos 可编译性"拆开看:
| 上游能力 | 依赖 | ohos 处置 |
|---|---|---|
KMPAuth 门面、KMPAuthBackend 注册表、AuthCredential / KMPAuthUser 模型、7 个类型化异常 | 纯 Kotlin + coroutines | 整体编译(进 KLIB) |
| Firebase 会话:REST 引擎(Identity Toolkit 全协议) | 纯 Kotlin + kotlinx-serialization + 2 个平台接缝 | 整体编译(接缝补 ohos actual) |
| Supabase 会话 | io.github.jan-tennert.supabase:auth-kt,无 ohos 制品 | 留盘不进构建 |
| Google / Apple / Facebook 凭据获取 | 平台原生 SDK + Compose | 留盘不进构建 |
rememberXxxSignInState 等 Compose 状态 | androidx.compose.runtime | 隔离进 composeMain 不编译 |
Demo 提供一个真实的认证会话,对应桥接的全部入口:
- 初始化(initialize):注入 Firebase Web 配置(apiKey / projectId / appId)与应用沙箱
filesDir,走公开 APIKMPAuth.initialize { firebase(...) }; - 注册 / 登录(signUp / signIn):
AuthCredential.EmailPassword经 Identity Toolkitaccounts:signUp/accounts:signInWithPassword兑换会话,登录后自动经accounts:lookup补全用户资料; - 匿名登录(signInAnonymously):
accounts:signUp无参形态,已有匿名会话则续用而非新建匿名账号; - ID Token(token):
currentUserIdToken(forceRefresh),缓存过期或强制刷新时经 Secure Token 服务securetoken.googleapis.com/v1/token换新; - 会话闭环:refresh token 以 JSON 文件落
filesDir,杀进程重启后自动恢复并在首次用 token 前刷新——证明是落盘而不是内存; - 账号生命周期:登出(清会话)、删除账号(不可逆)、重新认证(敏感操作前置)、重置密码邮件、邮件链接登录两步流;
- 版本握手(version):返回 fork 版本号,来自真实 Native 调用而非静态文字。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | kmpauth-core 与 kmpauth-firebase 的 commonMain + restEngineMain 全部文件原样编译,不提取、不重写;引擎与协议逐行零改动。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm("desktop") 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);Compose 状态面移入不注册的 composeMain;uihelper / providers / supabase / deprecated / sampleApp 留盘不进 settings。 |
| 桥接稳定 | 2 个 C ABI + UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| HTTP 纪律 | REST 引擎的传输接缝由"ArkTS 回注"实现:引擎挂起 → 请求冒泡 → 系统网络栈执行 → 响应回灌;.so 内零 TLS 依赖。 |
| 依赖纪律 | 版本主体与上游完全一致(coroutines / serialization 1.11.0),只把仓库换到 ohos 制品线(换线不换版),上游测试零改动全绿。 |
| UI 完整 | 单页覆盖全部桥入口:配置卡、账号卡、10 个操作按钮、状态 / 用户 / JWT 结果卡、HTTP 往返日志和版本握手。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit,不放任何第三方镜像站链接。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。KMPAuth 的 Compose 按钮容器与状态留在上游源码树(
composeMain),等社区 CMP 与上游版本对齐后整体接回;ArkTS Demo 是认证引擎的验收宿主。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块/依赖边界,确认"引擎纯 Kotlin、UI Compose"的切分线
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版
第 3 阶段:源集拆分 ── composeMain 隔离 6 个状态文件,PhoneVerificationUi 抽回 commonMain
第 4 阶段:ohos 引擎接线 ── 仿上游 wasmJs 五件套写 ohosMain actual(REST 引擎 + 文件存储 + 传输)
第 5 阶段:桥协议与回注 ── @CName C ABI + MiniJson + ArkTS 回注传输(suspendCancellableCoroutine)
第 6 阶段:ArkUI 宿主 ── DevEco Stage 工程、CMake NAPI、认证操作页,覆盖全部桥入口
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
kmpauth-core/ 认证引擎(进 ohos 构建,jvm("desktop") + ohosArm64 + ohosX64)
kmpauth-core/src/composeMain/ 上游 Compose 状态面(SignInState、rememberEmailAuthState 等 6 文件,ohos 不编译)
kmpauth-core/src/ohosMain/ 新增:loadPlatformBackends / isAndroidPlatform actual
kmpauth-core/src/desktopMain/ 新增:JVM 交叉验证 actual(ServiceLoader)
backends/firebase/kmpauth-firebase/ Firebase 会话后端(进 ohos 构建)
src/commonMain/ FirebaseAuthBackend expect + FirebaseBackendOptions + firebase() 配置 DSL
src/restEngineMain/ 上游原样:FirebaseRestAuthEngine(Identity Toolkit 全协议)
src/ohosMain/ 新增:REST 引擎 actual + 回注传输 + POSIX 会话存储 + 桥 + MiniJson + SpinLock
src/desktopMain/ 新增:JDK HttpClient 传输 + ~/.kmpauth 存储 + ServiceLoader 服务类
kmpauth-uihelper/ providers/* backends/supabase deprecated/ sampleApp/ build-logic/
留盘不进 settings.gradle.kts
example/nativeApp/ Kotlin/Native 动态库(@CName 出口)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :kmpauth-core、:backends:firebase:kmpauth-firebase 与 :example:nativeApp,rootProject.name = "ohos_kmpauth"。上游的 AGP 约定插件(build-logic)、Compose 插件、vanniktech 发布、二进制校验、dokka 全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven) |
| JDK | 21(Temurin 21.0.12,org.gradle.java.home 钉绝对路径) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm("desktop") 保留作交叉验证) |
| kotlinx | coroutines / serialization-json 1.11.0-0.1.0-07(ohos 制品线) |
| Gradle | 9.6.1(沿用上游 wrapper;2.4.20 线支持 Gradle 9.x) |
| DevEco product | 5.1.0(18) 兼容,API 26 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.kmpauth |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version # openjdk 21.0.12
1.3 源码依赖度盘点
| 模块 | 源集 | 依赖形态 | 处置 |
|---|---|---|---|
kmpauth-core | commonMain + 5 平台源集 | coroutines + compose runtime(api) | 引擎编译;compose 依赖随状态文件一起移出编译面 |
kmpauth-firebase | commonMain + nonWasmMain + restEngineMain + 5 平台源集 | core + serialization-json + GitLive(仅 nonWasmMain) | commonMain + restEngineMain 编译;nonWasmMain(GitLive)不进 |
| 其余 7 个上游模块 | - | Compose / 原生 SDK / supabase-kt | 留盘不进构建 |
关键发现是 expect 面极小。core 的 commonMain 只有 3 个 expect:loadPlatformBackends()(注册表发现)、isAndroidPlatform()、phoneAuthPlatformUiContext();firebase 的 restEngineMain 只有 1 个 expect:defaultFirebaseSessionStorage()——REST 引擎把 HTTP 抽象成了单方法接口 FirebaseRestTransport(suspend fun post(url, jsonBody): String),把会话存储抽象成了双方法接口 FirebaseSessionStorage(load / save)。这意味着 ohos 侧只需要补 4 个 actual + 1 套接缝实现,引擎零改动。
还有一个上游设计红利:REST 引擎本来就不是为鸿蒙写的——上游在 Desktop(GitLive firebase-java-sdk 无 auth 实现,issue #204)和 wasm(无 Firebase SDK 目标)已经在用它,ohos 是同一份引擎的第三个平台。错误映射(账号碰撞 KMPAuthUserCollisionException、需近期登录 KMPAuthRecentLoginRequiredException)、token 刷新、会话恢复的 stale 语义都是上游双端服役过的原件。
1.4 页面边界
标题区 KMPAUTH · OPENHARMONY / 版本握手行(3.0.5-ohos.1,来自真实 Native 调用)
Tab 切换 认证操作 / 关于
配置卡 apiKey / projectId / appId 三输入框 + filesDir 展示 + 初始化按钮
账号卡 email / password 输入框(密码框)
操作卡 注册 / 登录 / 匿名登录 / 当前用户 / 获取 ID Token / 刷新 Token /
重认证 / 重置密码邮件 / 登出 / 删除账号(10 按钮,按破坏性配色)
结果卡 状态行 + 当前用户卡(uid/email/displayName/providerId/isAnonymous/providerIds)+ JWT 全文(可复制)
日志卡 带时间戳的 HTTP 往返日志(→ POST … / ← 响应摘要,最新在前,上限 100 条)
关于页 版本握手、架构分层、HTTP 策略、会话持久化说明、仓库与社区链接
页面启动时先做版本握手,再自动注入 getContext().filesDir;未初始化点操作给 Toast 提示。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 ohos 的 kotlinx 制品。用 .pom / .module 逐个验证:
org.jetbrains.kotlin:kotlin-gradle-plugin:2.4.20-0.1.0-07 ✓ 200
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0-0.1.0-07 ✓ 200(.module 含 ohosArm64/ohosX64 变体)
org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0-0.1.0-07 ✓ 200
org.jetbrains.kotlin.multiplatform 插件 marker 2.4.20-0.1.0-07 ✓ 200
dev.gitlive:firebase-auth / io.github.jan-tennert.supabase:* ✗ 无 ohos 制品
三个关键结论:其一,kotlinx 版本主体与上游声明逐字相同(coroutines 1.11.0、serialization 1.11.0),只是仓库换线——commonMain 里一行依赖坐标都不用改;其二,serialization 编译器插件不需要:REST 引擎只用树 API(buildJsonObject / parseToJsonElement),编译面无一处 @Serializable;其三,GitLive 与 supabase-kt 无 ohos 制品,决定"只编 REST 引擎、不编 GitLive"的边界,与上游 wasm 的处境同构。
2.2 工具链钉版(为什么是 2.4.20-0.1.0-07)
与 KStore 同款结论:coroutines 1.11.0-0.1.0-07 的 stdlib 元数据是 2.4.20,旧钉法 Kotlin 2.2.21-1.0.0(qrose / haze 用的)编译期直接报 metadata is 2.4.0, expected 2.3.0 or less。工具链版本跟着依赖走:Kotlin 钉 2.4.20-0.1.0-07。上游源码本来就是 Kotlin 2.4.0 编的,2.4.20 同代,源码级兼容成本为零;Gradle 9.6.1 上游 wrapper 原样沿用,不像 2.2.21 旧线必须降 8.14.1。
settings.gradle.kts 把 eazytec 仓库写在最前:
pluginManagement {
repositories {
// HarmonyOS Kotlin distribution MUST come first
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
gradlePluginPortal()
google()
mavenCentral()
}
}
// dependencyResolutionManagement 同样置前
rootProject.name = "ohos_kmpauth"
include(":kmpauth-core")
include(":backends:firebase:kmpauth-firebase")
include(":example:nativeApp")
gradle.properties 钉 JDK 21 与版本号,并开启 kotlin.native.distribution.downloadFromMaven=true(KN 发行版含 ohos 交叉工具链从 Maven 镜像下载):
org.gradle.java.home=C:\\Program Files\\Eclipse Adoptium\\jdk-21.0.12.101-hotspot
version=3.0.5-ohos.1
kotlin.native.ignoreDisabledTargets=true
kotlin.native.distribution.downloadFromMaven=true
gradle/libs.versions.toml 收敛为 Kotlin + kotlinx 两条版本线,无 Compose、无 AGP、无发布插件。
2.3 编译真实引擎
kmpauth-core/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm("desktop") // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {
api(libs.kotlinx.coroutines.core)
}
commonTest.dependencies {
implementation(kotlin("test"))
implementation(libs.kotlinx.coroutines.test)
}
}
}
kmpauth-firebase 同构,但要多接一个中间源集:上游的 restEngineMain(REST 引擎)要挂到 desktop 与 ohos 两个目标下面,而 GitLive 的 nonWasmMain 不进:
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm("desktop")
ohosArm64()
ohosX64()
sourceSets {
val restEngineMain = register("restEngineMain") {
dependsOn(getByName("commonMain"))
dependencies {
// JSON runtime API only — no @Serializable, no compiler plugin.
implementation(libs.kotlinx.serialization.json)
}
}
getByName("desktopMain") { dependsOn(restEngineMain.get()) }
getByName("ohosMain") { dependsOn(restEngineMain.get()) }
commonMain.dependencies {
api(project(":kmpauth-core"))
}
// 上游 jvmTest 原样复用为 desktop 交叉验证套件(仅排除两个 jvmMain 专属用例文件)
getByName("desktopTest") {
kotlin.srcDir("src/jvmTest/kotlin")
kotlin.exclude("com/mmk/kmpauth/firebase/backend/DesktopWebAuthFlowPageTest.kt")
kotlin.exclude("com/mmk/kmpauth/firebase/backend/ManualE2ETest.kt")
dependencies {
implementation(kotlin("test"))
implementation(libs.kotlinx.coroutines.test)
}
}
}
}
实测:
.\gradlew.bat :kmpauth-core:compileKotlinOhosArm64 :kmpauth-core:compileKotlinOhosX64 # BUILD SUCCESSFUL
.\gradlew.bat :backends:firebase:kmpauth-firebase:compileKotlinOhosArm64 `
:backends:firebase:kmpauth-firebase:compileKotlinOhosX64 # BUILD SUCCESSFUL
引擎与 REST 协议的 commonMain / restEngineMain 文件一个没动。
2.4 Compose 面的处置:composeMain 隔离 + 类型抽回
core 的 commonMain 里有 6 个文件 import androidx.compose.*(SignInState、EmailAuthState、AnonymousAuthState、OAuthState、PhoneAuthState、LocalKMPAuthBackend,全是 remember* 状态面)。处置分三步:
- 搬移:6 个文件 git mv 到
src/composeMain/,该目录不注册进任何源集,ohos / desktop 都不编译,纯留档等社区 CMP 与上游对齐后接回;对应的 2 个 Compose 测试同步搬到src/composeTest/。 - 类型抽回:
PhoneVerificationUi接口声明在PhoneAuthState.kt里,但AuthProviderBackend.signInWithPhone的签名引用了它——把它抽回commonMain/auth/PhoneVerificationUi.kt(纯接口,零 Compose),搬移后的文件留注释指路。这是 qrose 适配里"Code128Type 抽回 commonMain"的同款手法。 - expect 保留:
phoneAuthPlatformUiContext()的 expect 跟着状态文件进了 composeMain,jvm 的 @Composable actual 无需处理(ohos 构建用desktopMain全新目录,不编译上游 jvmMain——后者的 phone actual 也是 @Composable)。
desktopMain 因此是全新最小目录:ServiceLoader 版 loadPlatformBackends + isAndroidPlatform()=false,不碰任何 Compose。
2.5 测试依赖的处置
commonTest 的依赖表对 ohos 测试编译可见,但 Kotlin/Native 没有 ohos 测试运行器。处置是两条纪律的组合:测试统一在 JVM 跑(desktopTest),根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
firebase 的上游 jvmTest 只依赖 commonMain + restEngineMain 符号,直接 srcDir 复用为 desktopTest;唯一例外是 DesktopWebAuthFlow 页面构建用例(测上游 jvmMain 的浏览器流页面,不进本构建)——拆到独立文件 DesktopWebAuthFlowPageTest.kt 后排除,上游构建行为不变。
第 3 阶段:桥协议建模
3.1 JSON 契约
com.mmk.kmpauth.core/** + com.mmk.kmpauth.firebase restEngine(上游引擎,原样编译)
↓ KMPAuth 门面 / KMPAuthBackend 注册表 / FirebaseRestAuthEngine
KmpAuthOhosBridge + MiniJson(扁平 JSON 进出,suspend 协程编排)
↓ UTF-8 JSON ⇄(httpRequest / httpResponse 回注)
N-API kmpauthDispatch() / kmpauthVersion()
↓
ArkUI Index.ets(认证操作页)
请求 17 个 op,覆盖引擎全部无 UI 面:
{"op":"initialize","apiKey":"...","projectId":"...","appId":"...","filesDir":"..."}
{"op":"currentUser"} / {"op":"signOut"} / {"op":"deleteAccount"} / {"op":"version"}
{"op":"signUp","email":"...","password":"..."} {"op":"signIn","email":"...","password":"..."}
{"op":"signInAnonymously"}
{"op":"signInWithIdToken","providerId":"google.com","idToken":"...","accessToken":null,"rawNonce":null}
{"op":"token","forceRefresh":false}
{"op":"reauthenticate","email":"...","password":"..."}
{"op":"sendPasswordResetEmail","email":"..."}
{"op":"sendSignInLinkToEmail","email":"...","url":"..."}
{"op":"isSignInWithEmailLink","link":"..."} {"op":"signInWithEmailLink","email":"...","link":"..."}
{"op":"poll"} {"op":"httpResponse","id":1,"body":"{...}"}
响应四种 kind,其中 httpRequest 是回注协议的核心:
{"ok":true,"kind":"result","op":"signIn","user":{"uid":"...","email":"...","isAnonymous":false,"providerIds":["password"]}}
{"ok":true,"kind":"result","op":"token","token":"eyJhbGciOiJSUzI1NiIs..."}
{"ok":true,"kind":"httpRequest","id":1,"method":"POST",
"url":"https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=...",
"body":"{...}","headers":{"Content-Type":"application/json"}}
{"ok":true,"kind":"pending"}
{"ok":false,"kind":"error","op":"signIn","error":"Firebase auth failed: EMAIL_NOT_FOUND","errorType":"IllegalStateException"}
ArkTS 侧一个固定循环吃掉所有 kind:
let res = this.callBridge(JSON.stringify(request));
while (res.kind === 'httpRequest') {
const body = await this.postJson(res.url, res.body); // @ohos.net.http
res = this.callBridge(JSON.stringify({ op: 'httpResponse', id: res.id, body: body }));
}
while (res.kind === 'pending') {
await this.sleep(50);
res = this.callBridge('{"op":"poll"}');
}
// res.kind === 'result' 或 'error'
一次业务操作可能多轮往返:登录 → accounts:lookup 补资料 → securetoken 刷新,ArkTS 只管循环,协议状态机全在 Kotlin。
3.2 传输反转:HTTP 为什么这样设计
REST 引擎的接缝是 FirebaseRestTransport(suspend post(url, body): String)。ohos actual 的实现是回注:
internal class OhosRestTransport : FirebaseRestTransport {
override suspend fun post(url: String, jsonBody: String): String =
withTimeout(60_000) {
suspendCancellableCoroutine { continuation ->
HttpPump.offer(PendingHttpPost(nextRequestIdValue(), url, jsonBody, continuation))
}
}
}
post() 挂起的瞬间就是交接点:HttpPump 单槽保管请求,下一次 dispatch 返回把它作为 httpRequest 冒泡给 ArkTS;httpResponse op 到达时 continuation.resume(body),引擎在协程里继续跑。三个边界全部闭合:
- 超时:60 秒无回灌 →
TimeoutCancellationException→ 失败 Result,桥不会挂死; - 互斥:同一时刻只允许一个业务操作(
busy保护),引擎每操作串行,单槽足够; - 线程:桥的编排状态由
SpinLock(AtomicIntcompareAndSet)保护——该 Kotlin 分支不带kotlin.concurrent.SynchronizedObject,而 kotlinx.coroutines 的 Mutex 是 suspend-only,自旋锁是临界区仅数个字段写时的最小正确解。
.so 因此零 TLS 依赖:证书校验、HTTP/2、DNS 全部交给系统网络栈,库侧只保留请求构造与响应解析——这正是"引擎在 Kotlin、IO 在宿主"的分层。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI;上游引擎抛出的类型化异常(碰撞、需近期登录、网络错误)在桥侧转成 {"ok":false,"error":...,"errorType":"KMPAuthUserCollisionException"} 文本通道,ArkTS 统一渲染。非法 JSON、缺 op、缺参数、不认识的 op、未初始化就操作、并发操作,全部走同一个 ok:false 通道。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libkmpauth_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohoskmpauth.so)
│ KMPAuthDispatch / KMPAuthVersion
▼
KmpAuthOhosBridge → KMPAuth → FirebaseRestAuthEngine
⇅ HTTP 回注(DisposeString 纪律)
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制认证语义,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
@CName("KMPAuthDispatch")
fun kmpauthDispatch(request: String): String = KmpAuthOhosBridge.dispatch(request)
@CName("KMPAuthVersion")
fun kmpauthVersion(): String = KmpAuthOhosBridge.VERSION // "3.0.5-ohos.1"
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
KMPAuthDispatch | kmpauthDispatch(req) | 17 个 op:初始化 / 注册 / 登录 / 匿名 / token / 生命周期 |
KMPAuthVersion | kmpauthVersion() | fork 版本握手(真机版本行的数据来源) |
4.3 C++ N-API 方法分发
static napi_property_descriptor desc[] = {
{"kmpauthDispatch", nullptr, NapiKmpAuthDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"kmpauthVersion", nullptr, NapiKmpAuthVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohoskmpauth_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohoskmpauth_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。
4.4 CMake 和类型声明
add_library(ohoskmpauth SHARED IMPORTED)
set_target_properties(ohoskmpauth PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(kmpauth_napi SHARED napi_init.cpp)
target_link_libraries(kmpauth_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohoskmpauth)
OHOS_ARCH 为空或 libohoskmpauth.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const kmpauthDispatch: (request: string) => string;
export const kmpauthVersion: () => string;
nm_modname="kmpauth_napi" 与 ArkTS import 名、index.d.ts 导出名三处一致。
第 5 阶段:ArkUI 宿主
5.1 注入路径和触发操作
aboutToAppear(): void {
this.version = kmpauthVersion(); // 版本握手:真实 Native 调用
this.filesDir = getContext(this).filesDir; // 沙箱路径,随 initialize 下发
this.log('版本握手: ' + this.version);
}
private initialize(): void {
this.run('initialize', {
apiKey: this.apiKey.trim(), projectId: this.projectId.trim(),
appId: this.appId.trim(), filesDir: this.filesDir,
});
}
run() 内部走 3.1 的回注循环,busy 期间互斥;未初始化点操作 → Toast 提示。
5.2 页面交互
认证操作 Tab 自上而下:配置卡(三输入框 + filesDir 展示 + 初始化)、账号卡(邮箱 / 密码)、操作卡 10 按钮(注册深绿 / 登录蓝 / 匿名灰 / token 橙 / 删除深红,语义与破坏性对齐)、结果卡(状态行 + 用户卡 + JWT 全文可复制)、日志卡(时间戳,→ POST 与 ← 响应 逐轮留痕,最新在前、上限 100 条)。关于 Tab 展示版本握手、ArkTS → NAPI → C ABI → Kotlin 引擎 → @ohos.net.http → Firebase 分层说明与 AtomGit 仓库 / 社区链接。
5.3 对比度和错误边界
主操作深绿按钮白字,破坏性操作红色系,ok:false 时错误原文与 errorType 进状态行与日志。Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志区;回注循环有 20 轮上限,防异常场景下的无限往返。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohoskmpauth)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/com/mmk/kmpauth/sample/ohos/
│ └── Bridge.kt @CName × 2(转发到库内 KmpAuthOhosBridge)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.kmpauth、versionName 3.0.5-ohos.1
└── entry/
├── build-profile.json5 externalNativeOptions + abiFilters [arm64-v8a, x86_64]
├── libs/{arm64-v8a,x86_64}/ linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
├── src/main/cpp/CMakeLists.txt IMPORTED 引入 KN so、$ORIGIN rpath
├── src/main/cpp/napi_init.cpp N-API 导出、参数检查、DisposeString
├── src/main/cpp/include/libohoskmpauth_api.h
├── src/main/cpp/types/libkmpauth_napi/{index.d.ts, oh-package.json5}
├── src/main/module.json5 requestPermissions: ohos.permission.INTERNET(唯一权限)
└── src/main/ets/pages/Index.ets ArkUI 验收页面
6.2 构建、签名和安装
.\gradlew.bat :kmpauth-core:desktopTest :backends:firebase:kmpauth-firebase:desktopTest # 29 项交叉验证
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example\harmonyApp
hvigorw assembleHap --mode module -p product=default -p buildMode=debug --no-daemon
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机 ~/.ohos/config,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组)。
hdc list targets -v
hdc -t <设备序列号> install -r \
entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b org.terminator.ohos.kmpauth -m entry
四、完整代码对照
4.1 整体架构
com.mmk.kmpauth.core/**(上游引擎原样编译)
│ KMPAuth 门面 / KMPAuthBackend 注册表 / AuthCredential / KMPAuthUser / 类型化异常
com.mmk.kmpauth.firebase commonMain + restEngineMain(上游原样编译)
│ FirebaseAuthBackend expect / FirebaseRestAuthEngine(Identity Toolkit 全协议)
ohosMain(新增接缝)
│ REST actual + OhosRestTransport(回注)+ POSIX 会话存储 + 自动注册
example/nativeApp Bridge.kt + 库内 KmpAuthOhosBridge + MiniJson
│ KMPAuthDispatch / KMPAuthVersion
libkmpauth_napi.so
│ kmpauthDispatch / kmpauthVersion
Index.ets
│ JSON parse + 回注 HTTP 循环 + @State 更新
▼
ArkUI 真机页面(配置 / 账号 / 操作 / 结果 / 日志)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_kmpauth、7 个上游模块移出 |
gradle/libs.versions.toml | Kotlin 2.4.20-0.1.0-07 + kotlinx 1.11.0-0.1.0-07 两条线,无 Compose/AGP/发布插件 |
build.gradle.kts(根) | 版本 3.0.5-ohos.1、全局禁用 Ohos*Test* 任务 |
kmpauth-core/build.gradle.kts | 纯 KMP:jvm("desktop") + ohosArm64 + ohosX64 |
kmpauth-firebase/build.gradle.kts | 同上 + restEngineMain 中间源集接线 + desktopTest 复用 jvmTest |
kmpauth-core/src/composeMain/ | 上游 6 个 Compose 状态文件(不编译,等 CMP 对齐接回) |
kmpauth-core/src/commonMain/.../PhoneVerificationUi.kt | 唯一抽回的纯接口(signInWithPhone 签名依赖) |
kmpauth-core/src/{ohos,desktop}Main/ | 4 个 expect 的最小 actual |
kmpauth-firebase/src/ohosMain/ | REST actual、回注传输、POSIX 会话存储、@EagerInitialization 注册、KmpAuthOhosBridge、MiniJson、SpinLock(9 个新文件,全仓新增源码面) |
kmpauth-firebase/src/desktopMain/ | JDK HttpClient 传输、~/.kmpauth 存储、ServiceLoader 服务类 + META-INF/services |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 验收页面(认证操作 + 关于) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(24 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | KMPAuth.signIn / signUp / signInAnonymously | 凭据兑换会话,suspend,内部走注册表分发 |
| 引擎 | KMPAuth.currentUserIdToken(forceRefresh) | JWT 获取;过期经 Secure Token 刷新 |
| 引擎 | KMPAuthBackend.registerDefault | 后端自动注册(ohos 走 @EagerInitialization) |
| REST | FirebaseRestAuthEngine.call() | Identity Toolkit 端点调用 + 错误码→类型化异常映射 |
| REST | FirebaseRestTransport.post | HTTP 接缝(ohos:回注;desktop:JDK HttpClient;wasm:fetch) |
| REST | FirebaseSessionStorage | 会话持久化接缝(ohos:filesDir JSON 文件) |
| Native | KMPAuthDispatch / KMPAuthVersion | C ABI JSON 导出 |
| N-API | kmpauthDispatch(req) / kmpauthVersion() | 向 ArkTS 暴露能力 |
| ArkTS | dispatch(op) | 触发操作、驱动回注循环、刷新 UI |
4.4 ArkTS 与 Kotlin 的边界
const res = await kmpauth('signIn', { email: 'a@b.c', password: 'secret' });
// res.ok === true;res.user.uid / email / providerIds 可直接渲染
// 杀进程重启后 kmpauth('currentUser') 仍返回同一 uid(filesDir 会话恢复)
// 库内 ohosMain:桥把 op 翻译成上游公开 API,一行认证逻辑都不写
"signIn" -> {
val email = required(op, req, "email"); val password = required(op, req, "password")
userResult(op) { KMPAuth.signIn(AuthCredential.EmailPassword(email, password)) }
}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:真编译引擎 + REST 后端,而不是 ArkTS 重写认证
认证协议的错误映射(EMAIL_EXISTS / CREDENTIAL_TOO_OLD_LOGIN_AGAIN → 类型化异常)、token 刷新链、会话恢复 stale 语义是上游双端(Desktop #204 / wasm)服役过的原件,重写必漂移且失去同步能力。REST 引擎的接缝只有"1 传输 + 1 存储",ohos 补两个 actual 就把整个后端接活——这比提取"零依赖内核"更保守:什么都没抽,自然什么都不漂。
决策 2:工具链升到 Kotlin 2.4.20-0.1.0-07,Gradle 沿用 9.6.1
与 KStore 相同的推导:coroutines 1.11.0-0.1.0-07 基于 stdlib 2.4.20 构建,旧钉法 2.2.21 读不了 2.4 元数据;上游本来就是 Kotlin 2.4.0,2.4.20 同代零降级。Gradle 方面 2.4.20 线支持 9.x,上游 wrapper 9.6.1 原样保留。
决策 3:composeMain 隔离 + 类型抽回,而不是砍 commonMain
上游 core 的 commonMain 混装"引擎 + Compose 状态"。整体不编等于丢引擎,整体都编等于拖 Compose。第三条路:状态面 git mv 到不注册的 composeMain(上游构建与 ohos 构建都看得见这批文件的归属),被引擎签名引用的 PhoneVerificationUi 抽回 commonMain。等社区 CMP 与上游 Compose 1.11 / Kotlin 2.4 对齐,把目录注册回源集即可整体复活,目录结构已按此预留。
决策 4:HTTP 走 ArkTS 回注,而不是在 .so 里链 TLS
Kotlin/Native 侧自做 HTTPS 要么拖 Ktor + curl 引擎(依赖矩阵爆炸、无 ohos 制品),要么手写 TLS(不可审计)。回注方案把 IO 完整交给系统 @ohos.net.http:证书校验、代理、HTTP/2 都是系统能力;库侧只保留请求构造与响应解析——认证语义 100% 在 Kotlin,字节传输 100% 在宿主。代价是桥协议从"同步一来一回"升级为"挂起-回灌"状态机,由 suspendCancellableCoroutine + 60 秒超时 + 单槽 HttpPump 闭合。
决策 5:desktopTest 交叉验证 + 测试近乎零改动
引擎逻辑在 commonMain / restEngineMain,JVM 与 ohos 跑同一份代码;29 项 desktopTest 里 25 项是上游原测试文件(FirebaseRestAuthEngineTest 24 项 + 发现机制 1 项),唯一改动是把一个 jvmMain 专属用例拆到独立文件再排除——上游构建行为不变。测试近乎零改动,交叉验证"引擎没漂"的说服力才成立。
决策 6:jvm(“desktop”) 而不是复用上游 jvmMain
上游 core 的 jvmMain/PhoneAuthState.jvm.kt 是 @Composable、firebase 的 jvmMain 引 GitLive——复用等于把 Compose 与 GitLive 拖进 ohos 构建。desktop 用全新最小源集(4 个 actual + JDK 传输 + ServiceLoader),桌面目标验证的恰好是"ohos .so 里跑的同一条 REST 路径"。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x86_64)、JDK 21(Temurin 21.0.12)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
.\gradlew.bat :kmpauth-core:desktopTest :backends:firebase:kmpauth-firebase:desktopTest
29 项用例全部通过(上游原测试文件):
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
FirebaseRestAuthEngineTest | 24 | REST 协议契约:请求体构造、响应解析、碰撞/需近期登录异常映射、token 刷新链、会话恢复 stale 语义、web-flow 会话收编 |
FirebaseBackendDiscoveryTest | 1 | ServiceLoader 自动发现后端(经 desktopMain 服务类) |
KMPAuthBackendTest | 2 | 注册表:默认后端、多后端、no-backend 失败语义 |
LoggerContractTest | 2 | 日志契约 |
依赖换线没有 API 漂移:coroutines / serialization 1.11.0 换成 -0.1.0-07 线后版本主体不变,上游测试照跑全绿——"换线不换版"的直接证据。
6.3 HAP 验证
:kmpauth-core:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:backends:firebase:kmpauth-firebase:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL(copyKnSo* 回填 entry/libs)
llvm-nm --defined-only: KMPAuthDispatch / KMPAuthVersion 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libkmpauth_napi.so)
CompileArkTS: finished(仅 deprecation 警告)
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
entry-default-unsigned.hap 内含双 ABI 各三件套(libkmpauth_napi.so 约 65KB / libohoskmpauth.so zip 后约 4.4MB(debugShared 未压缩约 9.4MB,含 Kotlin/Native 运行时与 coroutines)/ libc++_shared.so 约 1.3MB);module.json 确认唯一权限 ohos.permission.INTERNET。
6.4 功能用例
- 启动后标题行显示
3.0.5-ohos.1、日志出现版本握手: 3.0.5-ohos.1(来自真实 Native 调用,非静态文字); filesDir自动注入真机沙箱路径(/data/storage/el2/base/haps/entry/files)并随 initialize 下发;- 填入 Firebase Web 配置点初始化,返回
ok:true且 apiKey 尾 4 位回显; - 匿名登录:用户卡出现 uid、
isAnonymous:true,日志逐轮显示→ POST https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=…与← {"idToken":…}回注往返; - 邮箱注册 / 登录:用户卡显示 email、
providerIds:["password"];错误密码在状态行呈现INVALID_PASSWORD类错误原文; - 获取 ID Token:JWT 全文可复制;
forceRefresh时日志出现securetoken往返; - 杀掉应用重启后点当前用户:uid 不变(会话从 filesDir 恢复,恢复后的 token 首次使用前自动刷新);
- 登出后当前用户为空、会话文件删除;删除账号后 Firebase 控制台用户消失;
- 每步操作的 HTTP 往返按时间戳滚动留痕(
→/←成对)。
6.5 验证结论
认证引擎、REST 协议层、token 刷新链路、文件持久化、Native 动态库、C ABI、N-API、ArkTS 回注 HTTP、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的认证会话(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/KMPAuth.git
cd KMPAuth
export JAVA_HOME="/path/to/jdk-21"
./gradlew :kmpauth-core:compileKotlinOhosArm64 :backends:firebase:kmpauth-firebase:compileKotlinOhosArm64 # klib
./gradlew :kmpauth-core:desktopTest :backends:firebase:kmpauth-firebase:desktopTest # 29 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.kmpauth -m entry
hdc shell hilog | grep KmpAuth
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:kmpauth-core/:kmpauth-firebase真正编译成ohosArm64/ohosX64; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Kotlin 2.2.21 旧线编译 kotlinx
1.11.0-0.1.0-07报 metadata 2.4 不兼容——工具链版本由依赖制品的 stdlib 元数据反推,钉2.4.20-0.1.0-07; - Gradle 9.6 + 该 Kotlin 分支下
by getting/by registering从废弃升级为脚本编译错误,且注册自定义源集后不生成目标访问器——统一改getByName("desktopMain")/register("restEngineMain")字符串 API; - 该分支 stdlib 没有
kotlin.concurrent.SynchronizedObject/synchronized,AtomicLong也没有addAndFetch/fetchAndAdd——桥内自写SpinLock(AtomicIntcompareAndSet循环),请求号用 CAS 自增; - cinterop 的 POSIX API 需
@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class),否则 ohos 会话存储的 fopen/fread 全线报 opt-in 错误; - 上游
jvmTest混入了 jvmMain 专属类的用例(DesktopWebAuthFlow),desktopTest 编译失败——拆到独立文件再 exclude,上游构建行为不变; hvigorw assembleHap到 PackageHap 报spawn java ENOENT:打包工具是 jar,PATH 里没有 java——把 DevEco 自带 jbr(…\DevEco Studio\jbr\bin)加进 PATH 即过;- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free(); - 认证 Demo 的
module.json5别忘requestPermissions: ohos.permission.INTERNET——qrose 这类纯本地渲染库没有权限项,照抄它的模板会得到一个所有网络操作都失败的 Demo; - 签名材料由 DevEco 自动写入
build-profile.json5,证书绝不进入 AtomGit(仓库内保持空signingConfigs)。
8.2 已知问题
- Compose 按钮容器与
remember*状态不在(composeMain 留档),ohos 应用以 ArkTS 自绘 UI + 桥调用替代; - Google / Apple / Facebook 原生凭据流不在:SDK 无鸿蒙版本;
signInWithIdToken保留为兑换通道,应用层取得 ID Token 后即可登录; - Supabase 后端不在:supabase-kt 无 ohos 制品,待制品线出现后按 desktopMain 同款模式接入;
- 手机号登录不可用:REST 引擎需 reCAPTCHA(与上游 Desktop / wasm 同一限制);OAuth 浏览器流同理(
webFlowRunner = null); currentUserFlow不跨 C ABI:桥是请求-响应式,订阅式推送过不了 C 签名,ArkTS 以操作后读回观察状态;- HTTP 依赖 ArkTS 宿主回注:脱离宿主直接调
KMPAuthDispatch且不回灌httpResponse,60 秒后以超时错误结束(而非永久挂起); - android / js / wasmJs / apple 目标剥离(源文件留在树里),发布、dokka、apiCheck 一并剥离,本 fork 不发 Maven Central;
- HAP 安装必须使用与 bundle name
org.terminator.ohos.kmpauth匹配的签名。
8.3 未来优化方向
- 增加
currentUserFlow状态变化推送(N-API 线程安全回表),替代操作后读回; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 等 supabase-kt / 社区 CMP 的 ohos 制品线,分别解锁第二个后端与 Compose UI 面;
- 把回注传输协议(挂起-回灌状态机)沉淀为社区通用"宿主 HTTP"模式,供其他网络类 KMP 库复用;
- 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 kmpauth-core + kmpauth-firebase(commonMain + restEngineMain)
→ 验证 ohos kotlinx 制品线(coroutines / serialization)
→ 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 9.6.1 沿用)
→ 根构建替换(cmp_qrose 模式)+ composeMain 隔离 + PhoneVerificationUi 抽回
→ ohosArm64 / ohosX64 KLIB + ohosMain 接缝 actual
→ HTTP 反转:回注传输(suspendCancellableCoroutine + HttpPump + 60s 超时)
→ C ABI(@CName × 2)+ 17 op JSON 协议
→ C++ N-API(DisposeString 纪律)
→ ArkTS 回注循环 + ArkUI 真机单页验收(注册 / 登录 / 匿名 / token / 会话持久化)
9.2 封装层次
- 引擎层:上游原版
KMPAuth门面、KMPAuthBackend注册表、凭据 / 用户模型、类型化异常; - 协议层:上游原版
FirebaseRestAuthEngine(Identity Toolkit 请求构造、响应解析、错误映射、token 刷新、会话编解码); - Native 层:双 ABI 动态库,2 个 C ABI 输出 JSON,回注传输与 POSIX 会话存储在库内 ohosMain;
- N-API 层:参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:配置注入、回注 HTTP 循环、页面状态、日志与错误文本;
- DevEco 层:CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- expect 面决定适配成本:KMPAuth 全库编译面只有 4 个 expect、2 个接缝接口——上游架构分层(provider / backend / core)本身就是为"换平台实现后端"设计的,适配成本由上游架构质量决定,跨桥前先盘点 expect 清单,别急着写代码;
- 网络类库的答案是"传输反转":存储库(KStore)的 IO 落在 kotlinx-io,网络库的 IO 没有等价制品——把 HTTP 交给宿主系统栈、把协议留在 Kotlin,
.so零 TLS 依赖的同时认证语义一寸不让;挂起-回灌状态机(挂起点即交接点、超时兜底、单槽回灌)是这个模式的三要素; - 交叉验证要"同路径"而不是"同文件":desktop 目标不复用上游 jvmMain(那里是 GitLive 路径),而是与 ohos 走同一条 REST 路径——验证的是".so 里跑的代码",不是"另一份相似代码";29 项测试里 25 项上游原文件零改动,是"引擎没漂"的可信证明。
9.4 适配成果
当前适配已完成:
- KMPAuth
3.0.5真实认证引擎的双 ABI Kotlin/Native 编译(上游引擎文件零改动,新增源码集中在 ohos 接缝与桥); - HarmonyOS kotlinx 制品线在认证库上的完整落地(coroutines / serialization-json
1.11.0-0.1.0-07); - 2 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号)+ 17 op JSON 桥协议; - ArkTS 回注 HTTP 传输(
@ohos.net.http),.so零 TLS 依赖,单操作多轮往返自动衔接; - 会话文件持久化与重启恢复(filesDir + POSIX);
- DevEco Stage ArkUI 单页验收页面(配置、10 操作、用户 / JWT 结果卡、HTTP 往返日志、版本握手);
- 29 项 JVM 交叉验证全绿 + HAP 构建(双 ABI 六件套 so)+ 签名、设备安装和真机验证;
- AtomGit 项目文档、OpenHarmony README(中英双份)、博客文章和本地效果图。
适配过程 FAQ
以下问题来自
ohosArm64目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。
Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?
- 确认 DevEco Studio 版本与工程
build-profile.json5的compatibleSdkVersion一致; - 在
File > Settings > Build, Execution, Deployment > Ohpm配置可用的 ohpm 仓库镜像或代理; - 删除
oh_modules、oh-package-lock.json5与.hvigor缓存后重新 Sync; - 公司网络需要代理时,同时给 ohpm 与 hvigor 配置
http.proxy。
Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?
- 先在
example目录执行./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用linkDebugSharedOhosX64); - 确认
libxxx.so与libc++_shared.so已复制到ohos/entry/libs/<abi>/; - 检查
entry/build-profile.json5的abiFilters与设备 ABI 是否一致(真机arm64-v8a,模拟器x86_64); - 清理
entry/.cxx与entry/build后重试。
Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?
- 确认 CMake 的
add_library(<target> SHARED ...)名称与import ... from 'libxxx_napi.so'完全一致; - 解包 HAP,确认
libs/<abi>/libxxx_napi.so已打包; - 删除
entry/build、entry/.cxx与oh_modules后重新构建; - 检查
module.json5的srcEntry与页面路径是否正确。
Q4:真机安装报 signature verification failed 或 install failed?
- 在 DevEco Studio 的
Signing Configs配置证书、Profile 与密码; bundleName必须与签名 profile 中的 bundle name 一致;- 设备需加入 profile 的
deviceIds,调试证书有效期需覆盖当前时间; - 用
hdc list targets确认设备在线,再执行hdc install -r xxx-signed.hap。
Q5:应用启动白屏、闪退或 NAPI 调用崩溃?
- 用
hdc hilog | grep -i "napi\|kotlin"查看原生日志; - 确认
libc++_shared.so与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致; - 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用
XxxFree释放; - 在 DevEco 的 Native 调试配置中附加
lldb,定位空指针与越界访问。
Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?
- 使用鸿蒙发行版 Kotlin(如
2.2.21-1.0.0),并在settings.gradle.kts配置 eazytec 仓库; - Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
- 确认
kotlin { ohosArm64() }已应用,且local.properties的 SDK 路径有效; - 清理
~/.gradle/caches中对应版本的 Kotlin/Native 缓存后重试。
Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?
- 对齐 DevEco Studio、HarmonyOS SDK 与
build-profile.json5中的compatibleSdkVersion; - 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免
modelVersion与 IDE 不匹配; - 命令行构建时设置
DEVECO_SDK_HOME指向 DevEco 的sdk目录。
Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?
- 使用 DevEco 自带的 node 与 hvigor:
"$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...; - 设置
JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME; - 加上
--no-daemon排除守护进程缓存影响,并清理entry/build与.hvigor。
参考文档
更多推荐



所有评论(0)