为什么同一个应用里,Anthropic 和 GitHub 的 OAuth 能顺利走完,谷歌登录却卡在授权页?答案不在网络,而在谷歌的 OAuth 2.0 政策:开发者不得把谷歌授权请求"导向由开发者控制的嵌入式用户代理"。

打开网易新闻 查看精彩图片

Shelly 的做法是把谷歌 OAuth 链接通过 Custom Tabs 交给真正的 Chrome 进程处理,应用内路径则留给 Anthropic 和 GitHub,不做改动。要弄清这条链路,得先回答三个问题:拦截发生在哪一步、什么信号暴露了 WebView、桥接层如何把 URL 送出应用。

WebView 是怎么被认出来的

谷歌对"嵌入式用户代理"的定义相当宽:凡是能让开发者插入任意脚本、改变发往谷歌 OAuth 服务器的请求默认路由、或读取会话 Cookie 的软件库,都算在内。每个浏览环境还必须让用户能核验当前与谷歌 OAuth 服务器的连接,包括请求的 URI 和连接安全信息。

RFC 8252 的表述更直接:原生应用的授权请求"必须使用外部用户代理"。同一份 RFC 还指出,授权端点可以检测并拦截嵌入式用户代理,因为宿主应用有能力访问认证凭据。

站点识别 WebView 靠的是 User-Agent 里的 wv 标记,除非应用自己覆盖了 UA 字符串。早期行为中,WebView 会给每个请求加上 X-Requested-With,值为应用的 APK 名称。已公布的变更计划是从常规流量中移除这个头,同时允许开发者显式选择加入,并为依赖它的服务提供弃用 Origin Trial。

Shelly 的记录显示,谷歌 OAuth 在 2026-05-08 拒绝通过 X-Requested-With 检测渲染登录界面。受影响的范围是 Gemini 登录——它依赖谷歌 OAuth。提交 1c367c474 曾声称 Codex / Claude / Gemini 的 OAuth 都能在应用内完成、无需外部浏览器;而 2026-05-08 晚间的工作交接把 Gemini 登录列为第一阶段唯一遗留缺口,并称 1.2 阶段会解决它。

URL 是怎么被交给 Chrome 的

提交 1c367c474 交付了一套应用内 OAuth 桥接,由 xdg-open 垫片和 RN 文件队列组成。2026-05-08 发布的原生 xdg-open 二进制由 jni/shelly-xdg-open.c 编译为 libshelly_xdg_open.so。

这个二进制是 jniLibs / libDir 中的直接 execve 目标,从而绕开 Android 对 app_data_file 脚本的 binfmt_script 审计——该审计被三星 Knox 的 sepolicy 拒绝。HomeInitializer.kt 把 $HOME/bin/xdg-open 软链到该二进制,并导出 BROWSER。

URL 经由 ~/.shelly-deep-link-queue 传递,app/_layout.tsx 每 250ms 排空一次队列。提交 22e935084(2026-05-08,PR #50)在 RN 侧加入 1.2 阶段第一步的基础设施,设计上把 URL 的所有权留给 CLI,因此 PKCE、令牌交换和凭据写入都在 CLI 的 loopback 回调上完成,符合 RFC 8252。

2026-07-17 的提交 6bc581864 补了一张安全网:派发之前,app/_layout.tsx 会把队列中仍标记为"应用内"、但主机为 accounts.google.com 的行升级为"外部浏览器"。这防的是未来或第三方发射方忘记显式设置 authMode。对 accounts.google.com 的检测在本次提交之前就已存在于 shelly-xdg-open.c 中。

版本、默认值与边界

平台侧的事实来自谷歌和 Chrome 的官方页面。从 Android 17 起,Android WebView 的默认 User-Agent 字符串被缩减:操作系统、CPU 和 Build 信息变成静态的"Linux; Android 10; K",次版本、构建和补丁版本信息缩减为"0.0.0"。

另有三项只来自 Shelly 自己的记录:Knox 对脚本执行的拒绝、250ms 的队列排空周期,以及把 X-Requested-With 描述为无条件注入。最后这条说法出自 Shelly 2026-07-17 的提交。

其他可选路径

完全走外部浏览器(RFC 8252):在应用之外发起授权请求,通过重定向 URI 把响应送回。仍然存在的约束是 PKCE,公共原生客户端必须自行实现。

Auth Tab:从 Chrome 137 起,Auth Tab 可以直接替换现有的 Custom Tabs 认证集成。其 API 以 alpha 构建发布,依赖示例使用 androidx.browser:browser:1.9.0。数据通过回调到达,而不是标准 Custom Tabs 用来接收浏览器数据的 intent;认证此前所需的 AndroidManifest.xml intent 过滤器不再需要。

采用前要先看清重定向形式。完成时,标签页用 https 或自定义 scheme 向应用回调。启动方法接受两组参数之一:自定义的 redirectScheme,或用于 https 重定向的 redirectHost 与 redirectPath。走 https 时,需要数字资产链接验证。

代价是这个标签页去掉了标准 Custom Tab 的大部分定制选项。设备不支持 Auth Tab 时,会自动回退到 Custom Tabs。应用必须同时处理可能从 ActivityResultCallback 或 Activity intent 到达的数据。如果 AuthTabIntent 启动了回退、而当前浏览器不支持 Auth Tab,Custom Tab 关闭时应用会收到 Activity.RESULT_CANCELED。

动手前先自查这几项

  • 改完 User-Agent 后谷歌登录还失败吗?在 Shelly 里,单靠伪造 UA 没能绕过谷歌的 WebView 检测。
  • 应用是否设置了自定义 UA?用 setUserAgentString() 设置的应用不会发送缩减后的 UA;在 getUserAgentString() 后追加后缀的应用,默认部分仍以缩减形式发送。
  • 代码是否读取 WebView 的 User-Agent Client Hints?Android WebView 从版本 116 起支持,但仅限发送默认 UA 字符串的应用。
  • 用户的默认浏览器支持 Auth Tab 吗?这取决于默认浏览器及其版本,应用可用 CustomTabsClient#isAuthTabSupported() 查询。
  • 重定向用的是自定义 scheme,还是 https 主机加路径?Auth Tab 的启动方法只接受这两组参数之一。
  • 返回 URI 与授权会话中的 URI 完全一致吗?RFC 8252 要求精确匹配。