Skip to content

投放授权界面

授权界面由源站 nginx 在 auth.natlan.io 同源托管,不在 Cloudflare 上、没有自动 构建。任何改动(包括只改了 ui/)都要走这份流程。

停机:不需要 | 预计耗时:10 分钟(含验收)。

托管形态的理由见 internals/auth-hosting.md

发布顺序不能反

如果这次同时要改前端产物和 nginx 配置,必须先发前端、验一次、再发 nginx

只发 nginx 不发前端会直接打死人机验证:新配置下发的 CSP 依赖新 index.html 里的 __CSP_NONCE__ 占位符,旧产物没有那个占位符,验证码会转 20 秒后报超时。

反过来(只发前端不发 nginx)是安全的:那时页面上还没有 CSP,占位符原样留着不起作用。

日常只更新前端时,只做「步骤 1」即可。

前置检查

三项都是既有状态,正常情况下不用改,但发之前请确认:

检查期望怎么看
nginx 带 http_sub_moduledocker compose -p docker-compose exec nginx nginx -V 2>&1 | tr ' ' '\n' | grep sub_module
没开 gzip_static没开开了的话 sub_filter 改不动预压缩文件,占位符会原样发给浏览器
Cloudflare natlan.io 的 Rocket Loader / Auto Minify都是 offRocket Loader 会改写脚本标签,一开 CSP 必挂

另外,Cap 实例 challenges.natlan.io 的 CORS 白名单需含 https://auth.natlan.io

步骤 1:构建并投放前端产物

在仓根(分支 main):

bash
npm ci
npm run build:auth        # 产物在 apps/auth/dist/

发布前自检两条,都不满足就别上传

bash
grep -c __CSP_NONCE__ apps/auth/dist/index.html    # 必须是 2
grep -rl jsdelivr apps/auth/dist/                  # 必须无输出
ls apps/auth/dist/favicon.svg                      # 必须存在

第一条守的是 nonce 注入点(占位符出现在 script 标签属性与那行赋值,共两处); 第二条守的是「验证码不从 CDN 取东西」这个 CSP 前提。

上传到 nginx 的静态根目录(宿主机路径,容器内只读挂载为 /usr/share/nginx/www/auth):

bash
rsync -av --delete apps/auth/dist/ <部署>:/opt/.../docker/nginx/www/auth/

--delete 是必要的:产物文件名带内容 hash,不清理会越堆越多;index.html 不带 hash,会被正常覆盖。

投放后立刻验一次

bash
curl -s https://auth.natlan.io/authorize | grep -c __CSP_NONCE__
  • nginx 已经在注 nonce(常态)→ 期望 0,说明新产物就位且替换正常;
  • nginx 还没配 sub_filter(首次上线)→ 期望 2,占位符原样出现。

然后用一个完整的授权链接在浏览器里走一遍,人机验证应当正常通过。

步骤 2:nginx(只在首次上线或改策略时做)

编辑部署机上的 docker/nginx/conf.d/auth-natlan.conf

⚠️ 这个文件不在版本库里(后端仓只跟踪了 control-server.conf)。以服务器上那份 为准,手工应用下面的改动,不要整文件覆盖。

找到 location = /authorize

nginx
    location = /authorize {
        add_header Cache-Control "no-store" always;

        # index.html 里有两处 __CSP_NONCE__(script 标签属性 + 那行赋值),所以 once off
        sub_filter_once off;
        sub_filter '__CSP_NONCE__' '$request_id';

        # ⚠ 'unsafe-eval' 不是可选项:Cap 的 instrumentation 挑战脚本用 eval/Function,
        # 不放行它会静默死在沙箱 iframe 里、20 秒后超时,而主文档一条违规都不报。
        add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-$request_id' 'unsafe-eval' https://static.cloudflareinsights.com; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self' https://challenges.natlan.io https://cloudflareinsights.com; frame-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;

        try_files /index.html =404;
    }

其余位置(location /assets/location = /favicon.svg、各 /api 反代块) 不要动

校验并热加载(配置是只读挂载,不用重建容器):

bash
docker compose -p docker-compose exec nginx nginx -t
docker compose -p docker-compose exec nginx nginx -s reload

nginx -t 不过就停在这里,别 reload。

验收

四条,全部要过。

① 响应头有 CSP,且不缓存

bash
curl -sI https://auth.natlan.io/authorize | grep -iE 'content-security-policy|cache-control'

期望 Cache-Control: no-store,以及一条含 'nonce-<32 位十六进制>'Content-Security-Policy

② 头里的 nonce 与页面里的完全一致,占位符零残留

bash
curl -s -D /tmp/h.txt https://auth.natlan.io/authorize -o /tmp/b.html
grep -o "nonce-[a-f0-9]*" /tmp/h.txt | head -1      # 头里的
grep -o 'nonce="[a-f0-9]*"' /tmp/b.html | sort -u   # 页面里的(应当只有一个值)
grep -c __CSP_NONCE__ /tmp/b.html                   # 必须是 0

两个 nonce 值不同、或占位符还在 = 立刻回滚。

③ 每次请求的 nonce 都不一样

bash
for i in 1 2 3; do curl -sI https://auth.natlan.io/authorize | grep -io "nonce-[a-f0-9]*"; done

三行必须互不相同。相同说明响应被缓存了,no-store 没生效。

④ 人工走一遍真实授权流

这条自动化替代不了。 用一个完整的 /authorize?client_id=… 链接打开,输入 ASN, 点人机验证,确认方块变成橙色对勾。同时开着 DevTools Console,不应出现任何 Refused to … 开头的报错

如果账号里有 passkey 用户,顺带让其中一位真人试一次 passkey 登录。

页面里 content_main.js 之类的报错是浏览器扩展产生的,与本站无关,可忽略。

验证码卡在转圈超过 20 秒后报错,就是 CSP 拦了东西——回滚,并按 troubleshoot-csp.md 排查。

回滚

前端产物:投放上一版 dist/。产物文件名带内容 hash,回滚不会撞缓存。

nginx:只回 nginx 即可,前端产物可以留着(没有 CSP 时它照常工作)。删掉 location = /authorize 里新增的三行(sub_filter_oncesub_filteradd_header Content-Security-Policy),保留 Cache-Controltry_files,然后:

bash
docker compose -p docker-compose exec nginx nginx -t
docker compose -p docker-compose exec nginx nginx -s reload

回滚后 curl -s https://auth.natlan.io/authorize | grep -c __CSP_NONCE__ 应当回到 2