HSTS 只是 HTTPS 部署的一小部分。更常见的事故是:Django 不知道自己在 HTTPS 后面。这篇模拟 TLS 终止代理,把 SECURE_PROXY_SSL_HEADER 的作用、被伪造的风险、以及 SECURE_SSL_REDIRECT 造成的重定向循环跑出来,再用真实的 TLS(自签名证书)验证 HSTS 头。
[没有配置 SECURE_PROXY_SSL_HEADER]
没有代理头 → is_secure=False scheme=http
X-Forwarded-Proto: https → is_secure=False scheme=http
X-Forwarded-Proto: http → is_secure=False scheme=http
[SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')]
没有代理头 → is_secure=False scheme=http
X-Forwarded-Proto: https → is_secure=True scheme=https
X-Forwarded-Proto: http → is_secure=False scheme=http
↑ 注意第二行:头是谁发的 Django 不知道。如果客户端能直连 Django(或者代理不清除这个头),任何人都能自称 https。request.is_secure() 影响很多东西:安全 cookie、CSRF 的 Origin 检查(第 123 篇)、build_absolute_uri 的协议、HSTS 头。
第 1 次(代理每次都用 http 转发给 Django)→ 301 https://localhost/ 第 2 次(代理每次都用 http 转发给 Django)→ 301 https://localhost/ 第 3 次(代理每次都用 http 转发给 Django)→ 301 https://localhost/ 第 4 次(代理每次都用 http 转发给 Django)→ 301 https://localhost/ 第 5 次(代理每次都用 http 转发给 Django)→ 301 https://localhost/ 浏览器跟随 Location 再请求,代理仍然以 http 转给 Django,所以永远是 301:重定向循环 配置了代理头,代理带上 X-Forwarded-Proto: https → 200 is_secure=True scheme=https
如果代理本身已经做了 http → https 重定向,Django 这边就不必再开 SECURE_SSL_REDIRECT(这是设计建议,本文没有测)。
HTTPS GET / → 200 is_secure=True scheme=https | Strict-Transport-Security: max-age=31536000; includeSubDomains 明文 HTTP 发到同一个 TLS 端口 → RemoteDisconnected 没有验证的事:浏览器收到 HSTS 之后的行为(记住 max-age、自动把 http 升级成 https、证书错误时不能点击继续),这些发生在浏览器里。