Service Worker 撞上 ForwardAuth:容器日志面板登录死循环排障
系列第三篇,一个典型的「curl 全绿但浏览器依然是坏的」排障案例。问题的迷惑性在于:每次命令行测试都通过,真实浏览器一打开却掉进重定向循环,容易被连续误判成「已经修好了」。核心教训只有一句:curl 不执行 JavaScript,更不执行 Service Worker。 文中域名、版本号均为示例。
现象:卡在「登录校验」的死循环
Dozzle 是个容器日志查看器,部署在 logs.example.com,前面挂了 Traefik + Authelia ForwardAuth(见系列第二篇)。问题表现是:
- 浏览器访问
logs.example.com,未登录或会话过期后,页面无限卡在「登录校验」——导航挂起、转圈、网络面板里重定向请求循环刷屏,就是进不去。 - 诡异的是,同样认证配置的 Traefik Dashboard(
dashboard.example.com)却完全正常。 - 更诡异的是,用 curl 端到端测整个 ForwardAuth 流程——未登录 302、登录拿 Cookie、回跳 200——全部通过。
curl 全绿 + 同配置的另一个服务正常,这两个「证据」很容易让人连续多次误判成「已经修好了」,然后被浏览器现实打脸。
为什么 curl 会骗你
先说清楚这个最关键的认知:curl / WebFetch 这类工具只走 HTTP 请求-响应链,它不执行页面里的 JavaScript,更不会注册和运行 Service Worker。
Dozzle 的前端是一个 PWA。它加载后会注册一个 Service Worker(/sw.js),这个 SW 一旦注册就持久驻留在浏览器里,跨会话存活。curl 拿到的只是第一层 HTML 外壳,链条走到 Authelia 返回的登录页 HTML 就停了——从 curl 的视角看「拿到了 200/302,正常」。但真实浏览器里,那个驻留的 SW 会在背后发起一堆 curl 根本模拟不出来的请求。
这就是为什么这类问题容易被反复误判:验证手段(curl)和故障发生的层面(浏览器 + SW)根本不在一个维度上。
用真实浏览器复现,定位根因
换成真实浏览器 + DevTools 的 Network / Application 面板复现,真相才浮出水面:
- Dozzle 的 SW(
/sw.js,CACHE_NAME=dozzle-v10.9.0)注册后持久驻留。 - 会话缺失/过期时,SW 会做更新检查,并请求
/assets/*等静态资源。 - 这些请求同样经过 Traefik,被 ForwardAuth 判定未认证,跨域 302 重定向到
auth.example.com。 - 浏览器跟随重定向,最终拿到的是 Authelia 的登录页——
Content-Type: text/html的 HTML 外壳,而不是它期待的 JS/CSS。 - Chrome 拒绝把
text/html当 Service Worker 脚本解析(MIME 类型错误),叠加跨域重定向限制和 SW 生命周期钩子,导航被卡死/陷入循环。
一句话根因:SW 的更新检查与静态资源请求被认证中间件劫持,拿回 HTML 而非 JS,触发 MIME 错误 + 重定向循环。
为什么 Traefik Dashboard 同配置却正常
对比之下答案很清楚:Traefik Dashboard 没有注册 Service Worker。它就是个普通的受保护页面,未登录时干净地 302 到门户、登录后正常加载,不存在「后台有个 SW 在偷偷发被劫持的请求」这回事。同样的 ForwardAuth 配置,有没有 SW 是天壤之别。 这也反过来印证了根因就在 SW。
解法:用路由优先级放行静态资源
思路是让 SW 脚本和静态资源绕过认证(它们本来就不含敏感数据),而真正敏感的容器日志接口继续受保护。Traefik 的 router 支持 priority,正好用来做这件事。
在 File Provider(services.yml)里,为这个 Host 新增一个更高优先级、不挂认证中间件的 router,专门放行静态资源;原来的主 router 显式降低优先级并保留 authelia-forward,继续保护 / 和 /api/*:
http:
routers:
# 高优先级静态资源路由:不挂认证中间件,未登录也返回真实 JS/CSS
dozzle-static:
rule: "Host(`logs.example.com`) && (Path(`/sw.js`) || PathPrefix(`/assets`) || Path(`/favicon.ico`) || Path(`/manifest.json`) || PathPrefix(`/logo`))"
entryPoints:
- websecure
priority: 100
service: dozzle
tls:
certResolver: letsencrypt
# 主路由:显式降优先级,保留 ForwardAuth,保护 / 和 /api/*(敏感容器日志)
dozzle:
rule: "Host(`logs.example.com`)"
entryPoints:
- websecure
priority: 50
middlewares:
- authelia-forward
service: dozzle
tls:
certResolver: letsencrypt
要点:
dozzle-static的priority: 100高于主路由的priority: 50,Traefik 会优先匹配它,让/sw.js、/assets/*等未登录也能拿到 200 + 正确的 JS/CSS MIME 类型。- 主路由
priority: 50保留authelia-forward,敏感的容器日志/和/api/*依然受认证保护——放行静态资源不等于敞开日志。 - 精确列出需要 bypass 的静态路径(
/sw.js、/assets、/favicon.ico、/manifest.json、/logo),不要用宽泛的通配把整个域都放行。
已注册的 SW 会自愈吗
会。容易担心需要手动去浏览器 unregister SW、清 Site Data 才能让修复生效,实测发现不用:服务端的静态路由 fix 生效后,下次访问时 SW 的更新检查能正常拿到真正的 /sw.js,MIME 正确、不再被劫持,链条自愈,页面干净地跳转到登录页。这一点必须以真实浏览器实测确认。
真实浏览器多轮验证
修复后的验证也必须在真实浏览器里做,覆盖关键场景:
- 干净环境首次访问:未登录 → 干净 302 到 Authelia 门户 → 登录 → 回跳 Dozzle 正常加载。
- SW 已注册 + 会话过期:这是原来必崩的场景。现在 SW 更新检查能拿到真 JS,会话过期时干净跳转到登录页,不再死循环。
- 自愈验证:不做任何手动清理,直接访问,确认从故障态自动恢复。
DevTools 的 Network 面板里,/sw.js 和 /assets/* 现在返回 200 + application/javascript / text/css,不再有跨域 302 到 auth 的记录——这是修复到位的直接证据。
普适经验
- PWA / Service Worker 相关的问题,必须用真实浏览器复现和验证。 curl 通过 ≠ 浏览器通过。凡是「curl 全绿但用户说坏了」,第一反应就该怀疑客户端脚本/SW 层面。
- 认证反代 + 带 SW 的前端(PWA) 是一个高风险组合。SW 的后台请求会被认证中间件无差别劫持,拿回 HTML 而非 JS/CSS,引发 MIME 错误和重定向循环。任何这类组合都值得预先排查。
- 用路由优先级排除静态路径绕过认证,是解决这类问题的通用手法:给静态资源开一条高优先级、无认证的通道,敏感接口留在受保护的主路由上。既修好了 SW,又不牺牲安全性。
- 对比法排障:找一个「配置相同但表现正常」的参照物(这里是无 SW 的 Traefik Dashboard),差异点往往就是根因所在。
延伸话题:如何给 Django 站点零回归地加上明暗模式——同样是「必须真实浏览器验证」,但坑在 CSS 特异性和对比度上。