鸿蒙真机排障实录:把「登录成功」变成「真的登录了」

xiezitai · 2026-10-22 03:00 · 16 次阅读
苹果Apple鸿蒙登陆账号

鸿蒙真机排障实录:把「登录成功」变成「真的登录了」

一次 Sign in with Apple 接入,连续踩了六个坑。服务端日志从头到尾都是「登录成功」,用户看到的却是「没登录」。记录下来,省下下次的半夜。

背景

Flutter + OpenHarmony 项目,接入苹果登录。鸿蒙 / Android 没有苹果原生 SDK,只能走 Web 授权流程(OAuth2 + form_post)。

架构一句话:App 内 WebView 加载苹果授权页 → 用户授权 → 苹果把 code / id_token 以跨域 POST 提交到服务端 callback → 服务端验签、建号、签发 JWT → 把登录态交回 App → App 写本地会话、跳主页。

看起来平平无奇,实际每一环都断过一次。

六个断点

# 现象 真因 修法
1 服务端报「JWKS 中找不到对应公钥」,刷新到的公钥数量是 0 苹果把 id_token 签名密钥轮换成了 RSA / RS256,而代码只收 EC / P-256,RSA key 全被过滤掉 JWKS 解析同时支持 RSA(n/e)与 EC(x/y)
2 服务端只收到一个没有参数的 GET redirect_uri 用了不带 www 的域名,nginx 301 跳 www,POST 被降级成 GET 并丢弃 body redirect_uri 必须是最终处理请求的地址,中间不能有跳转
3 改用系统浏览器后,服务端登录全成功,App 拿不到登录态 服务端 302 到自定义 scheme 后,登录态能否带回 App 取决于系统浏览器 / openLink 行为,不可控 改回 App 内 WebView
4 WebView 路线下,登录态依然丢失 服务端 302 到自定义 scheme 时,WebView 导航拦截器完全不回调(详见下文) 服务端改返回 200 HTML 结果页,数据内嵌在页面里
5 结果页数据取到了(参数齐全),点「确认」却没反应 确认动作放在网页按钮上,要靠 JS 通道 / 深链拦截把点击传回 App,这两条通道在鸿蒙 Web 组件上都不可靠 确认动作改为 Flutter 原生 UI
6 终于跳到主页,但所有接口全部调不通 装的是 debug 包,kReleaseMode == false → baseUrl 走 http://localhost,手机上根本没有本地后端 真机验收改用 release 构建

关键结论一:WebView 跨端通信有明确的可靠性分级

这是本次最大的认知收获,按可靠性排序:

✅ 可用 —— App 主动执行 JS 取页面数据

final raw = await controller.runJavaScriptReturningResult(
  "(function(){var el=document.getElementById('apple-login-result');"
  "return el?el.textContent:'';})()");

方向是 App → 网页,App 掌握主动权,什么时候取、取几次都能控。本次登录数据就是靠它拿到的,真机验证稳定。

⚠️ 不可靠 —— JS 通道(addJavaScriptChannel + 网页 postMessage)

注册依赖原生 WebView 实例已经创建,动态注册的时机并不总有保障。

⚠️ 不可靠 —— 网页 location.href = 自定义scheme 再由 App 拦截

对自定义 scheme 的导航是否回调 onNavigationRequest,行为不确定。

❌ 完全不行 —— 服务端 HTTP 302 到自定义 scheme

HTTP/1.1 302
Location: myapp://auth/apple?success=true...

这一跳不会触发 onNavigationRequest / onPageStarted(Android 的 shouldOverrideUrlLoading 同理)。这两个回调只对「页面发起的导航」生效——链接点击、JS 跳转——对服务端重定向不回调。登录态就是在这一环丢的,代码从头到尾没机会执行。

正确做法:自定义 scheme 回跳不要直接 302,改成返回一个 200 HTML 结果页,把登录数据内嵌进页面(比如放在 <script type="application/json"> 里),由 App 用 runJavaScriptReturningResult 主动取走。

关键结论二:关键交互不要放在网页侧

第 5 个坑值得单独说。当时我在结果页放了一个「确认返回 App」按钮,onclick 里同时做了两件事:调 JS 通道通知 App、跳自定义 scheme 深链。看起来是双保险,实际上两条通道都不可靠,点了没反应。

原则:凡是 WebView 里能取到的数据,一律用 runJavaScriptReturningResult 取到 App 侧;确认、提交、跳转这类关键交互,必须用原生 UI 完成,不要交给网页按钮再想办法把结果传回来。

改完之后,确认卡片是 Flutter 原生 Widget,数据早就在 App 手里了,点一下直接写会话、跳主页,不再依赖任何跨端通道。

关键结论三:真机调试的特殊性

第 6 个坑最气人——症状是「跳到主页了但所有接口调不通」,极其容易误判成「登录态没写进去 / token 无效」,然后在会话代码里反复白查。

真相是构建模式:

static String get baseUrl {
  if (kReleaseMode) return 'https://<生产域名>';
  else return 'http://localhost';   // debug 包走这条
}

PC 上「开发态」意味着连我本机的服务;真机上「开发态」意味着连一个不存在的服务。手机上没有本地后端,请求打的是手机自己的 localhost。

通用原则:凡是按 kReleaseMode / kDebugMode 分支的配置——baseUrl、日志级别、埋点开关、mock 数据——debug 包装到真机上都会走「开发态」分支,语义和 PC 完全不同。真机验收前先确认构建模式,别只看 UI 跑不跑得通。

但不要为了省事把 dev baseUrl 改成生产地址:localhost 在 Web 本地调试时是刚需(flutter run -d chrome 与本地后端同机)。正确姿势是加编译期开关(--dart-define 之类),保持默认行为不变。

判别包是不是真 release(鸿蒙):解压 hap 看 libs/arm64-v8a/libflutter.so,release 引擎约 16MB、debug 引擎约 39MB;release 包有 libapp.so(AOT)、没有 kernel_blob.bin。

关键结论四:归因纪律

第 2 个坑导致了一条错误结论。当时现象是「服务端只收到无参 GET」,我据此判断「鸿蒙 Web 组件不支持 form_post」,改走系统浏览器;系统浏览器路线又因为深链回传不可控而失败——两个 bug 叠加,产生了第三次误判,绕了好几圈。

教训两条:

  1. 先排除域名 / 跳转层,再归因到客户端框架。 nginx 301、CDN 重定向、http→https 跳转,都会悄悄改变请求方法和 body。
  2. 推测必须标注为推测,并用可复现的实验或日志证伪后再下结论。把猜测当事实陈述,会让自己后续的所有推理都建立在沙子上。

关键结论五:真机上怎么看日志

排查期间踩到的观测障碍:

收尾

六个断点全部修完,链路打通。几个遗留的小优化(头像用了 http 明文地址、另一个第三方登录漏写了两个字段)不影响主流程,留着慢慢收拾。

最大的收获不是某个具体 API,而是三条方法论:跨端通道要分级信任、关键交互别放网页侧、真机验收先确认构建模式。

评论

还没有评论,登录后来说两句。