# Service Worker 撞上 ForwardAuth：容器日志面板登录死循环排障

> 作者: Lazybones | 日期: 2026-09-05

# 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 面板复现，真相才浮出水面：

1. Dozzle 的 SW（`/sw.js`，`CACHE_NAME=dozzle-v10.9.0`）注册后持久驻留。
2. 会话缺失/过期时，SW 会做**更新检查**，并请求 `/assets/*` 等静态资源。
3. 这些请求同样经过 Traefik，被 ForwardAuth 判定未认证，**跨域 302 重定向到 `auth.example.com`**。
4. 浏览器跟随重定向，最终拿到的是 Authelia 的登录页——`Content-Type: text/html` 的 HTML 外壳，**而不是它期待的 JS/CSS**。
5. 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/*`：

```yaml
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 正确、不再被劫持，链条自愈，页面干净地跳转到登录页。**这一点必须以真实浏览器实测确认。**

## 真实浏览器多轮验证

修复后的验证也必须在真实浏览器里做，覆盖关键场景：

1. **干净环境首次访问**：未登录 → 干净 302 到 Authelia 门户 → 登录 → 回跳 Dozzle 正常加载。
2. **SW 已注册 + 会话过期**：这是原来必崩的场景。现在 SW 更新检查能拿到真 JS，会话过期时干净跳转到登录页，不再死循环。
3. **自愈验证**：不做任何手动清理，直接访问，确认从故障态自动恢复。

DevTools 的 Network 面板里，`/sw.js` 和 `/assets/*` 现在返回 200 + `application/javascript` / `text/css`，不再有跨域 302 到 auth 的记录——这是修复到位的直接证据。

## 普适经验

1. **PWA / Service Worker 相关的问题，必须用真实浏览器复现和验证。** curl 通过 ≠ 浏览器通过。凡是「curl 全绿但用户说坏了」，第一反应就该怀疑客户端脚本/SW 层面。
2. **认证反代 + 带 SW 的前端（PWA）** 是一个高风险组合。SW 的后台请求会被认证中间件无差别劫持，拿回 HTML 而非 JS/CSS，引发 MIME 错误和重定向循环。任何这类组合都值得预先排查。
3. **用路由优先级排除静态路径绕过认证**，是解决这类问题的通用手法：给静态资源开一条高优先级、无认证的通道，敏感接口留在受保护的主路由上。既修好了 SW，又不牺牲安全性。
4. **对比法排障**：找一个「配置相同但表现正常」的参照物（这里是无 SW 的 Traefik Dashboard），差异点往往就是根因所在。

延伸话题：如何给 Django 站点零回归地加上明暗模式——同样是「必须真实浏览器验证」，但坑在 CSS 特异性和对比度上。
