WSL 里的 CLI,为什么能直接唤起 Windows 浏览器
原创 · 约 24 分钟阅读 · 阅读 --

WSL 里的 CLI,为什么能直接唤起 Windows 浏览器

作者: Alex Xiang


古董级程序员,大厂出来后一直在创业公司,现在仍活跃在一线做 AI 相关的开发。更完整的更新写在微信公众号「字与码」:工作经历、对新技术的想法,以及这些年走弯路的记录,会不定期发在那里。若觉得博客对你有用,欢迎顺手关注。

今天做一个统一授权功能,其中有一项很具体的要求:命令行程序不能再让用户手工配置一串长期密钥,它应该像现在常见的开发者 CLI 一样,输入登录命令,去浏览器完成身份验证和授权,然后回到终端继续工作。

我先写了一个很小的 Python Demo。在 WSL 的终端里运行后,Windows 上的默认浏览器直接弹了出来。

import webbrowser

webbrowser.open(authorize_url)

这段代码简单得有点可疑。Python 运行在 Linux 里,Chrome 或 Edge 明明装在 Windows 上,中间没有写 explorer.exe,也没有判断 WSL,为什么它知道该去宿主机开浏览器?

更有意思的是,浏览器能打开,并不意味着授权一定能顺利回到 CLI。浏览器最后访问 127.0.0.1 时,Windows 和 WSL 对“本机”的理解可能并不一致。这正好把 CLI 登录背后的几层机制都翻了出来。

WSL 中的 CLI 唤起 Windows 宿主机浏览器完成授权

先把完整流程走一遍

这个 Demo 使用的是 OAuth 2.0 Authorization Code + PKCE。为了不带入某个具体产品,下面把服务都写成通用地址:

https://login.example.com/authorize
https://login.example.com/token
https://api.example.com/resource

CLI 启动时会做几件事:

  1. 生成一次性的 statenonce 和 PKCE code_verifier
  2. code_verifier 计算 code_challenge
  3. 在本机启动一个短命的 HTTP 回调监听器;
  4. 打开系统浏览器访问授权地址;
  5. 用户在浏览器登录并确认授权;
  6. 浏览器跳转到本机回调地址并携带一次性 code
  7. CLI 校验 state,再用 code + code_verifier 换取 token;
  8. 使用 access token 调用真正的资源接口。

一个精简后的授权地址大概是这样:

https://login.example.com/authorize?
  response_type=code&
  client_id=desktop-cli&
  redirect_uri=http://127.0.0.1:8765/callback&
  code_challenge=...&
  code_challenge_method=S256&
  state=...

这里最值得保留的不是 OAuth 参数表,而是两个边界:密码永远只输入到可信的系统浏览器里,CLI 只接收短命授权码;拿到授权码也不能直接换 token,还必须持有当前进程生成的 PKCE verifier。

RFC 8252 专门讨论原生应用授权,推荐用外部浏览器作为 user-agent,并要求公共原生客户端支持 PKCE。桌面应用使用 HTTP loopback redirect,也是规范明确支持的做法。

webbrowser 没有浏览器,它只是一个调度器

Python 的 webbrowser 属于标准库,不负责下载网页,更不会在 Python 进程里嵌入 Chromium。它只做一件事:找到当前环境认可的“打开 URL”方式,把地址交出去。

可以直接检查它选中了什么:

import webbrowser

browser = webbrowser.get()
print(type(browser))
print(vars(browser))

我这里得到的是:

<class 'webbrowser.BackgroundBrowser'>
{'name': 'xdg-open', 'args': ['%s'], 'basename': 'xdg-open'}

所以真正执行的相当于:

xdg-open https://login.example.com/authorize

webbrowser.open() 返回 True,只代表成功启动了这个外部命令,不代表网页加载完成,更不代表授权回调已经成功。

xdg-open 为什么知道 Windows 在外面

xdg-open 来自 Freedesktop.org 的 xdg-utils。它不是一个固定指向 Firefox 的软链接,而是一段兼容多种桌面环境的 Shell 脚本:在 GNOME、KDE、Xfce、macOS、Cygwin 和 WSL 中,它会选择不同后端。

本机的脚本会检查两件事:

grep -q microsoft /proc/version &&
command -v explorer.exe >/dev/null &&
DE=wsl

确认在 WSL 后,URL 最终交给:

rundll32.exe url.dll,FileProtocolHandler "https://login.example.com"

rundll32.exe 是 Windows 程序。url.dll,FileProtocolHandler 会按 Windows 的 URL 协议关联找到默认浏览器。因此这条链路并不依赖 Linux 里安装 Chrome,也不是 WSLg 在转发一个 Linux GUI 窗口。

xdg-open 经由 WSL Interop 调用 Windows URL Handler,回调则经过另一条网络路径

底层能力来自 WSL Interop。Microsoft 在 Windows 10 Insider Build 14951 中加入了 Windows/Ubuntu 互操作,时间是 2016 年 10 月。从那以后,WSL 可以直接运行:

notepad.exe
ipconfig.exe
powershell.exe
explorer.exe

Microsoft 的互操作文档说明,这些程序以当前 Windows 用户身份运行,也会像普通 Windows 程序一样出现在任务管理器中。

不过,xdg-open 自己识别 WSL 要晚很多。上游 Git 历史显示,WSL 适配最早写于 2021 年,2023 年合入,随后进入 2024 年发布的 xdg-utils 1.2.0。2024 年的另一次修改把 URL 打开方式调整为现在的 rundll32.exe url.dll,FileProtocolHandler

换句话说,这里叠了两代能力:

2016:WSL 可以运行 Windows EXE
2024:xdg-open 正式把这项能力包装成通用的“打开 URL”体验

能打开浏览器,为什么回调还会卡住

这是开发 Demo 时最容易产生错觉的地方。

打开浏览器的方向是:

WSL 进程 → WSL Interop → Windows 浏览器

授权结束后的方向却是:

Windows 浏览器 → http://127.0.0.1:8765 → WSL CLI

前一条是进程互操作,后一条是网络访问。它们完全不是同一个机制。

在普通桌面 Linux 中,浏览器和 CLI 通常共享一套 loopback 网络,这个做法很自然。但 WSL、Remote SSH、Dev Container 和云主机里,浏览器与 CLI 可能位于不同网络边界。此时浏览器地址栏里的 localhost 指向浏览器所在机器,不一定是 CLI 所在环境。

一个可用的 CLI 至少应处理以下细节:

  • 回调只接受预期路径;
  • 必须验证当前授权流程的 state
  • 重复打开的旧授权页不能抢走新会话;
  • 回调监听器应短命,用完立即关闭;
  • WSL 环境可以允许调整监听地址;
  • 自动回调不可达时,提供粘贴完整 callback URL 的兜底;
  • 远程服务器场景不要假设用户能访问服务器的 loopback。

这不是为了把 Demo 写复杂,而是为了避免“浏览器已经显示授权成功,终端却永远等着”的半完成状态。

Codex 到底是不是只用设备码登录

不是。

当前 Codex CLI 同时具备两套思路。常规交互登录会启动短命的本地回调服务器,默认代码中使用类似 http://localhost:1455/auth/callback 的地址,并尝试打开系统浏览器。它和上面的 Demo 属于同一类设计。

当前 CLI 也提供:

codex login --device-auth

这是设备授权流程。它更适合无头服务器、Remote SSH、容器以及 localhost 回调不可靠的 WSL 环境。把“Codex 使用设备码”理解为“Codex 永远不会自动打开浏览器”并不准确,更准确的说法是:Codex 为不同运行边界提供了不同登录路径。

我本机当前版本的帮助中,也能看到 --device-auth 是一个明确选项,而不是唯一入口。

设备码为什么绕开了 localhost

OAuth 2.0 Device Authorization Grant 由 RFC 8628 定义。它最初面向智能电视、打印机和输入受限设备,但很适合远程 CLI。

流程是:

  1. CLI 向授权服务器申请 device_code 和短 user_code
  2. 终端显示登录网址与短码;
  3. 用户在任意一台有浏览器的设备上打开网址并输入短码;
  4. CLI 按服务端给出的 interval 轮询 token endpoint;
  5. 用户批准后,下一次轮询返回 token。

浏览器回调授权与设备码授权的两条完整路径

它不需要浏览器回到 CLI,因此没有 loopback、端口占用、Windows 防火墙和远程端口转发这些前提。CLI 甚至可以运行在一台只有 SSH 终端的服务器上,用户用手机完成登录。

代价也很明确:

  • 多了一步打开网址或输入短码;
  • CLI 需要轮询,并正确处理 authorization_pendingslow_down 和过期;
  • 设备码可能被用于诱导授权,页面必须明确告诉用户正在授权哪台设备;
  • 企业环境可能禁用设备码,或者只允许少数受控应用使用。

RFC 8628 特别提醒了远程钓鱼与 session spying 风险。短码不是密码,但也不能把陌生人发来的设备码当作普通验证码输入。成熟的授权页应该展示应用、设备和权限范围,让用户确认终端上看到的代码与网页一致。

两种方式怎么选

如果 CLI 明确运行在用户本机,浏览器与进程共享可靠的 loopback,Authorization Code + PKCE 的体验最好:一条命令,浏览器自动弹出,点完授权直接回终端。

如果 CLI 可能运行在远程服务器、容器、WSL 网络隔离环境或纯文本设备上,设备码更稳。它把“浏览器在哪里”从协议前提中拿掉了。

实际产品不必二选一。我更倾向于:

场景默认方式备用方式
本机 Windows/macOS/Linux浏览器回调 + PKCE设备码
WSL 本地开发先尝试浏览器回调设备码或粘贴回调
Remote SSH/云主机设备码SSH 端口转发
Dev Container/Codespaces设备码IDE 自动端口转发
智能电视/打印机/IoT设备码QR 完整验证地址
CI/CD工作负载身份不应使用人工设备码

最后一行很重要。CI 不是“没有浏览器的人”,它是没有人的工作负载。自动化任务应该使用短期工作负载凭证、OIDC federation、managed identity 或受控 service account,不应该让流水线等某个人去网页点授权。

这套互操作还能提高哪些 WSL 工作效率

xdg-open 的价值不只在 OAuth。理解了“Linux 发起、Windows 承接”以后,可以把很多开发动作压缩成一条命令。

打开本地文档和报告

构建完 HTML 报告后直接打开:

xdg-open ./coverage/index.html

xdg-open 会先用 wslpath 转换路径,再交给 Windows 关联应用。测试报告、静态文档、导出的 PDF 都适用。

在浏览器里预览本地服务

开发服务器启动后:

python -m http.server 8080
xdg-open http://localhost:8080

前提仍然是 Windows 能访问对应端口。打开浏览器与端口可达是两件事,脚本应先做健康检查,再决定是否自动打开。

从 CLI 跳到系统设置或管理页面

初始化完成后直接打开账号设置、文档、监控页或部署结果页,比打印一条很长的 URL 更自然。对于第一次运行的 CLI,可以把“配置完成”和“下一步去哪”连起来。

让 Linux 工具调用 Windows 默认应用

除了 URL,还可以让 WSL 把文件交给宿主机:

xdg-open architecture.pdf
xdg-open benchmark.xlsx
explorer.exe .

这种方式适合偶发查看,不适合把高频 Linux 工作流长期搬到 /mnt/c。代码和高频小文件仍应放在 WSL 文件系统里,需要查看时再交给 Windows 应用。

给远程任务生成“一次性接力入口”

设备码思路还能用于 CLI 配对、MCP 服务授权、远程 Notebook、开发板初始化和临时运维控制台。核心不是那串短码,而是把“资源所在设备”和“用户完成身份验证的设备”解耦,同时用短时、限权、可撤销的凭证连接起来。

最后留下的设计原则

这次实现统一授权,最方便的瞬间是 WSL 里一行 Python 直接弹出 Windows 浏览器;真正值得记住的,却是它失败时暴露出来的边界。

webbrowser 只是调度器,xdg-open 只是适配层,WSL Interop 只负责启动 Windows 进程。浏览器回调是否能回来,属于另一条网络路径。OAuth 的 state、PKCE 和短命授权码,又属于安全边界。

把这些层分开后,CLI 登录方案就不难选:本机优先使用浏览器回调,让体验尽量无感;远程和无头环境使用设备码,不强行依赖 localhost;自动化环境则使用工作负载身份,不把人的登录流程塞进流水线。

一个好用的 CLI,不应该要求用户理解这些细节。但写 CLI 的人必须理解。

参考资料