WSL 里的 CLI,为什么能直接唤起 Windows 浏览器
古董级程序员,大厂出来后一直在创业公司,现在仍活跃在一线做 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 登录背后的几层机制都翻了出来。

先把完整流程走一遍
这个 Demo 使用的是 OAuth 2.0 Authorization Code + PKCE。为了不带入某个具体产品,下面把服务都写成通用地址:
https://login.example.com/authorize
https://login.example.com/token
https://api.example.com/resource
CLI 启动时会做几件事:
- 生成一次性的
state、nonce和 PKCEcode_verifier; - 由
code_verifier计算code_challenge; - 在本机启动一个短命的 HTTP 回调监听器;
- 打开系统浏览器访问授权地址;
- 用户在浏览器登录并确认授权;
- 浏览器跳转到本机回调地址并携带一次性
code; - CLI 校验
state,再用code + code_verifier换取 token; - 使用 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 窗口。

底层能力来自 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。
流程是:
- CLI 向授权服务器申请
device_code和短user_code; - 终端显示登录网址与短码;
- 用户在任意一台有浏览器的设备上打开网址并输入短码;
- CLI 按服务端给出的
interval轮询 token endpoint; - 用户批准后,下一次轮询返回 token。

它不需要浏览器回到 CLI,因此没有 loopback、端口占用、Windows 防火墙和远程端口转发这些前提。CLI 甚至可以运行在一台只有 SSH 终端的服务器上,用户用手机完成登录。
代价也很明确:
- 多了一步打开网址或输入短码;
- CLI 需要轮询,并正确处理
authorization_pending、slow_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 的人必须理解。
参考资料
- RFC 8252:OAuth 2.0 for Native Apps
- RFC 8628:OAuth 2.0 Device Authorization Grant
- Microsoft:在 WSL 中运行 Windows 工具
- Microsoft WSL Build 14951 发布记录
- OpenAI Codex CLI 登录服务器源码
- OpenAI:Codex CLI 与 Sign in with ChatGPT
- xdg-utils:WSL support commit
- xdg-utils:使用 Windows URL Handler 的修改
微信公众号
欢迎关注「字与码」
如果这篇文章对你有用,也欢迎在微信里继续关注后续更新。
X / Twitter
关注 @ax2_zicode
更即时的技术观察、新文章提醒和一些短想法会发在 X 上。