安全响应头与 HSTS:用 Worker 一次配全
Cloudflare 会自动加几个头:CF-RAY(请求标识,排查问题时报给客服或贴日志用)、NEL 与 Report-To(网络错误上报)、Server: cloudflare、Alt-Svc(声明 HTTP/3 可用)。这些是平台行为,你改不了。
真正决定浏览器行为的那几个头——Content-Security-Policy、Strict-Transport-Security、X-Frame-Options、Referrer-Policy、Permissions-Policy——不会自动出现。它们要么在源站加,要么在 Worker 里加。这篇走 Worker 这条路:代码里集中写一份,部署后抓响应头确认。
为什么放在 Worker:静态站没有源站配置可改,Pages 的 _headers 文件目前支持的头有限,而 Worker 里就是一个 Response 构造参数,改起来最直接。
1. 一个真实部署的 Worker
export default {
async fetch(request) {
const headers = {
"content-type": "text/html; charset=utf-8",
"strict-transport-security": "max-age=31536000; includeSubDomains",
"x-content-type-options": "nosniff",
"x-frame-options": "DENY",
"referrer-policy": "strict-origin-when-cross-origin",
"permissions-policy": "geolocation=(), microphone=(), camera=()",
"cross-origin-opener-policy": "same-origin",
"content-security-policy":
"default-src 'self'; img-src 'self' data:; script-src 'self'; " +
"style-src 'self'; frame-ancestors 'none'; base-uri 'self'"
};
return new Response("<!doctype html><title>headers demo</title><h1>ok</h1>", { headers });
}
};
wrangler deploy 的输出:
Total Upload: 0.96 KiB / gzip: 0.52 KiB
Worker Startup Time: 3 ms
Uploaded cf83-security-headers (3.83 sec)
Deployed cf83-security-headers triggers (2.77 sec)
https://cf83-security-headers.hyysn.workers.dev
抓回来的响应头(Invoke-WebRequest,200,92 字节响应体):
Alt-Svc: h3=":443"
CF-RAY: a58314f78d1fe2f7-HKG
Content-Security-Policy: default-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self'; frame-ancestors 'none'; base-uri 'self'
Content-Type: text/html; charset=utf-8
cross-origin-opener-policy: same-origin
Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
permissions-policy: geolocation=(), microphone=(), camera=()
Referrer-Policy: strict-origin-when-cross-origin
Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[...]}
Server: cloudflare
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
一共 13 行头:8 行来自上面的代码(含 Content-Type),另外 5 行(Alt-Svc、CF-RAY、Nel、Report-To、Server)是 Cloudflare 自动加的,与代码无关。
2. 每个头解决什么问题
| 头 | 作用 | 建议值 |
|---|---|---|
| Strict-Transport-Security | 让浏览器以后只走 HTTPS | max-age=31536000; includeSubDomains |
| Content-Security-Policy | 限制脚本、样式、图片、iframe 的来源 | 从 default-src 'self' 起步,按需放行 |
| X-Content-Type-Options | 禁止浏览器猜 MIME 类型 | nosniff |
| X-Frame-Options | 禁止被别站 iframe 嵌入 | DENY 或 SAMEORIGIN |
| Referrer-Policy | 控制跨站时带多少来源信息 | strict-origin-when-cross-origin |
| Permissions-Policy | 关闭不需要的浏览器能力 | geolocation=(), microphone=(), camera=() |
| Cross-Origin-Opener-Policy | 隔离窗口引用,防跨窗口攻击 | same-origin |
X-Frame-Options 与 CSP 的 frame-ancestors 功能重叠,前者给老浏览器用,两个都写不冲突。X-XSS-Protection 已经不需要了,现代浏览器移除了这个过滤器,写上反而可能引入旧漏洞。
把这一组头写在 Worker 里还有个好处:可以按路径返回不同策略。/api/* 只需要 nosniff 与 HSTS,页面路由才需要完整 CSP。静态站也可以用 Pages 部署,_headers 文件能覆盖基础几项,但 CSP 这类需要按环境变化的头,Worker 更灵活,两种部署方式的差别见 72-pages-vs-workers-assets.md。
3. HSTS 的参数与上线顺序
Strict-Transport-Security: max-age=31536000; includeSubDomains 的意思是:一年内,这个域名的所有子域只能用 HTTPS 访问。浏览器记住之后,用户在地址栏输入 http:// 会被本地改写成 https://,连请求都不会发出去。
这就带来一个不可逆的问题:max-age 生效期间,如果某个子域的 HTTPS 挂了,用户连"降级到 HTTP"这个选项都没有。所以上线顺序反过来:
第一步,max-age=300 观察一天,确认所有子域都能 HTTPS。 第二步,改成 max-age=86400,观察一周。 第三步,加到一年,再考虑 includeSubDomains。 第四步,确认一年内不会再回退,才加 preload 并提交到浏览器预加载列表。
preload 是最难回退的一步:列表是编进浏览器安装包的,移除要等下一个版本。小站没有必要走这一步。
另外,HSTS 只在 HTTPS 响应里生效,HTTP 响应带的 HSTS 头会被浏览器忽略。开了 HSTS 之后,Always Use HTTPS 也应当打开,否则首次访问仍是 HTTP。
4. CSP 的写法与踩坑
CSP 是最容易把站点写坏的一个头。从最严格开始,逐步放行:
default-src 'self';
img-src 'self' data:;
script-src 'self';
style-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self'
四个常见坑:
内联脚本被拦。<script>...</script> 与 onclick="..." 都属于内联,script-src 'self' 不放行它们。要么把脚本抽成外部文件,要么用 nonce(每次请求生成随机值,写进 CSP 与 script 标签)。静态站更简单:全抽成外部文件。
内联样式被拦。style="..." 属性需要 style-src 'unsafe-inline',或者改用 class。很多 UI 库依赖内联样式,先看浏览器 console 的报错再决定。
第三方资源被拦。Google Fonts、统计脚本、Turnstile(接入方式见 81-turnstile.md)都要单独放行。Turnstile 需要 script-src 与 frame-src 里加 https://challenges.cloudflare.com,否则控件加载不出来。
上线前用 Content-Security-Policy-Report-Only 跑一周,只报告不拦截,看 console 与上报端点里的违规记录,确认没有遗漏再切成强制模式。
如果站点用了多线程 WASM 或 SharedArrayBuffer,还需要 Cross-Origin-Embedder-Policy: require-corp。这个头会连带影响所有跨域资源,加载图片、字体、iframe 都要带 CORP 头,加之前先确认依赖清单。
5. 验证
curl -sI https://example.com/ | grep -Ei 'strict-transport|content-security|x-frame|referrer'
四个头都在输出里就说明生效。注意 curl -I 发的是 HEAD 请求,如果 Worker 只对 GET 返回头,用 curl -s -D - -o /dev/null。
Cloudflare 控制台也能配一部分:SSL/TLS → Edge Certificates 里有 HSTS 开关(与 82-ssl-tls-modes.md 里的 Always Use HTTPS 同一个页面),Rules → Transform Rules 可以改响应头。控制台改的好处是不用维护代码,坏处是版本不可追溯、多环境难同步。纯静态站用控制台更省事;已经有 Worker 的站点,写在代码里更清楚。
常见问题
加了 CSP 之后页面白屏,怎么快速定位?
打开浏览器 console,CSP 违规会打印被拦的资源与指令名。临时把 Content-Security-Policy 改成 Content-Security-Policy-Report-Only,页面恢复可用的同时还能看到违规记录。
HSTS 开了之后能把网站改回 HTTP 吗?
在 max-age 到期前不能,浏览器不会发出 HTTP 请求。清浏览器 HSTS 缓存(chrome://net-internals/#hsts)只能解决你自己的机器,用户端无法操作。所以 max-age 从小到大加。
这些头对 SEO 有影响吗?
正面影响。HTTPS 与 HSTS 是排名因素之一,CSP 不直接影响排名但能减少被挂马的风险。要注意的是 Referrer-Policy 设得过严会丢失来源数据,strict-origin-when-cross-origin 是折中值。
小结
安全响应头是一组"配一次、长期生效"的设置。Cloudflare 只负责平台级的那几个,业务级的要自己写。Worker 里集中写一份是最省事的方式:改一处、所有路径生效、可进版本库。
顺序上先做 HSTS 的小 max-age 试跑,再做 CSP 的 Report-Only 观察,最后才收紧。这两个头都能把站点弄挂,而它们的收益是长期的、不会立刻看见的。
数据来源与检索时间
检索时间 2026-11-10。
| 来源 | URL | 对应数据 |
|---|---|---|
| Cloudflare 定价页 | https://www.cloudflare.com/plans/ | 免费计划包含 Universal SSL 与边缘证书设置 |
| 边缘证书文档 | https://developers.cloudflare.com/ssl/edge-certificates/ | HSTS 开关位置与参数 |
| Workers 限额 | https://developers.cloudflare.com/workers/platform/limits/ | 免费档请求额度(决定这套头的成本可忽略) |
核验说明
- Worker 为本机真实部署并已删除:
wrangler deploy输出、启动耗时、访问 URL 原样记录;响应头由Invoke-WebRequest实际抓取,状态码 200,响应体 92 字节,两处 JSON 路径(/与/headers.json)都验证过。 - 部署的 Worker 名为
cf83-security-headers,验证完执行wrangler delete删除,账号内未留资源。 - 未执行:无 Zone 权限,控制台里的 HSTS 开关与 Transform Rules 未配置;CSP 的 Report-Only 观察流程未在真实站点跑过;HSTS 未在自有域名开启(避免把测试域名锁死)。
- 未验证:各浏览器对
Permissions-Policy的支持差异未逐一测试;文中建议值是按通用实践给的,不是抓包对照的结果。