外观
授权页为何独立托管
回答的问题:auth.natlan.io 为什么是四个站里唯一不在 Cloudflare Worker 上的; 这个主机名被 OIDC 协议钉死了哪些部分;把它挪走要付什么代价。
1. 结论
auth.natlan.io 整个主机名由源站 nginx 服务:授权界面的静态产物与认证服务 (auth-server)在同一个来源下,前端始终用相对路径请求 /api/v1/*,因此零 CORS。
这不是历史遗留。2026-08-10 曾把它整体迁到 Cloudflare Worker、验收通过,当天又整体 退回,理由是一条边界判断:授权服务是一个独立应用,它的 API 不应挂进消费方(控制面) 的 API 之下。 Worker 方案要成立就必须回源到 api.natlan.io/auth/*——那正是这种并置。
2. 这个主机名不可能是纯前端
auth.natlan.io 是 OIDC 的 issuer:
issuer https://auth.natlan.io
authorization_endpoint https://auth.natlan.io/authorize
token_endpoint https://auth.natlan.io/token
userinfo_endpoint https://auth.natlan.io/userinfo
jwks_uri https://auth.natlan.io/.well-known/jwks.json被协议钉死的只有发现文档那一条。 OIDC Discovery 1.0 §4 规定它必须位于 「issuer 字符串直接拼上 /.well-known/openid-configuration」的路径——那个 URL 是从 issuer 推导出来的,不是被通告的。issuer 本身也动不了:它进每个 ID Token 的 iss, RP 按 OIDC Core §3.1.3.7 精确匹配,改了等于让所有已签发令牌失效。
/token、/userinfo、jwks_uri 不受这条约束——它们在发现文档里是绝对 URL,写成 https://api.natlan.io/auth/token 完全合规。它们目前仍在这个主机名上,是两个非协议 原因:
- 后端
discovery_document()把所有端点都从issuer一个基址拼出来,没有独立的端点 基址配置; - 已取过发现文档的 RP 手里是旧 URL。
WebAuthn 的 RP ID 也取自 token_issuer,仍是 auth.natlan.io——动它会让所有已注册的 passkey 凭据作废。
3. 浏览器侧不该改成跨源调用
这条是实测结论,不是直觉:
| 方案 | 代价 |
|---|---|
授权页直连 api.natlan.io(跨源) | 跨源预检 446ms |
| 经一跳回源 | 中位差 28ms |
跨源方案更慢,还平添一份 CORS 白名单要维护。服务端到服务端那部分才值得省—— RP 后端调 /token 是真·跨机房流量。
4. 后端的双挂载
后端在 2026-08 把三个服务统一改成由 api.natlan.io/{auth,control,registry}/v1 承载。 认证服务因此同时挂在两处:
auth.natlan.io的根(/api/v1/*、/token、/.well-known/*)——授权页在用的;api.natlan.io/auth/*(_PROTOCOL_MOUNTS = ("", "/auth"))——服务端到服务端的入口。
前端只用第一处。两个前缀当前都通,但 apps/auth/src/lib/api.ts 里写的是 /api/v1/* 且必须保持同源相对路径,否则同源前提失效。
反过来说,nginx 不该给 auth-server 剥前缀:前缀由 auth-server 自己的双挂承担。
5. 代价与已知事项
- 不受 CF Build watch paths 覆盖。 改了
ui/之后另外三站会自动重建,这个站不会 ——要手工npm run build:auth并重新投放。这是最容易被遗忘的一点。 index.html的__CSP_NONCE__占位符不能删。 nginx 靠sub_filter逐请求把它 换成$request_id,同一个值也进 CSP 响应头。删了验证码会静默超时——迁往 Worker 那次删过一回,那正是当时最难查的故障。auth-natlan.conf不在版本库里。 后端仓只跟踪了control-server.conf。 以服务器上那份为准,改动要手工应用。
6. 迁移路上验证出来、与部署方式无关的事实
这四条不随「退回 nginx」失效,下次再碰这块时可直接引用:
- 只有发现文档被协议钉死(第 2 节)。其余端点是发现文档里的绝对 URL,协议上可以 挂到别处。
- 浏览器侧不该跨源(第 3 节,446ms vs 28ms)。
unsafe-eval是 Cap 的硬需求,被拦时主文档一条违规都不报,只表现为 20 秒后 超时。见 captcha.md。- nonce 必须用十六进制,base64 的
=padding 会截断不带引号的 HTML 属性。
另外两条是操作层面的观察,同样值得留着:
- Cloudflare 给已有外部 DNS 记录的主机名挂 Worker 自定义域会被拒 (
Hostname … already has externally managed DNS records)。把「删 A 记录」与 「PUT /workers/domains」放进同一次 API 调用,可以把无 DNS 的空窗压到最小。 - 前端源码在两种部署形态之间往返后,重建产物与线上正在服务的那份逐字节相同—— 说明这条路上没有产生不可逆的改动。