Skip to content

授权页为何独立托管

回答的问题: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/userinfojwks_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」失效,下次再碰这块时可直接引用:

  1. 只有发现文档被协议钉死(第 2 节)。其余端点是发现文档里的绝对 URL,协议上可以 挂到别处。
  2. 浏览器侧不该跨源(第 3 节,446ms vs 28ms)。
  3. unsafe-eval 是 Cap 的硬需求,被拦时主文档一条违规都不报,只表现为 20 秒后 超时。见 captcha.md
  4. nonce 必须用十六进制,base64 的 = padding 会截断不带引号的 HTML 属性。

另外两条是操作层面的观察,同样值得留着:

  • Cloudflare 给已有外部 DNS 记录的主机名挂 Worker 自定义域会被拒 (Hostname … already has externally managed DNS records)。把「删 A 记录」与 「PUT /workers/domains」放进同一次 API 调用,可以把无 DNS 的空窗压到最小。
  • 前端源码在两种部署形态之间往返后,重建产物与线上正在服务的那份逐字节相同—— 说明这条路上没有产生不可逆的改动。