HTTP 响应头是服务端和客户端之间的”合同条款”。写错了,轻则缓存失效导致用户总是加载慢,重则 CORS 报错让前端完全无法工作,或者 CSP 把自己的 JS 拦掉导致页面白屏。
误用一:Cache-Control 逻辑搞反
错误写法
Cache-Control: no-cache, max-age=0
本意是”不要缓存”,实际效果是”每次都要验证缓存”,服务器回 304 时浏览器还是用缓存。
正确写法
# 完全禁止缓存(敏感页面)
Cache-Control: no-store
# 条件缓存(HTML 页面,内容可能变化)
Cache-Control: no-cache
# 强缓存(带 hash 的静态资源)
Cache-Control: max-age=31536000, immutable
Cache-Control 指令速查
| 指令 | 含义 |
|---|---|
no-store | 不存储,每次重新下载 |
no-cache | 存储,但每次使用前必须验证 |
max-age=N | 存储 N 秒,期间不验证直接用 |
immutable | 内容永远不变,永远不发条件请求 |
must-revalidate | 过期后必须验证,不允许使用过期缓存 |
private | 只允许浏览器缓存,不允许代理缓存 |
public | 浏览器和代理都可以缓存 |
最佳实践:HTML 文件用 no-cache(每次验证,内容变化时立即生效);带 hash 的 JS/CSS/图片用 max-age=31536000, immutable(永久缓存,hash 变了就是新 URL)。
误用二:CORS 只处理了正常请求,忘了预检
问题现象
Access to fetch at 'https://api.example.com' from origin 'https://app.example.com'
has been blocked by CORS policy: Response to preflight request doesn't pass access
control check: It does not have HTTP ok status.
根源
浏览器在发实际请求前,自动发了一个 OPTIONS 请求(预检)。服务端路由没有注册 OPTIONS 方法,框架默认返回 405 Method Not Allowed。
修复
Express.js:
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://app.example.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
return res.sendStatus(204); // 预检直接返回 204
}
next();
});
Nginx:
location /api/ {
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always;
add_header Access-Control-Allow-Headers 'Authorization, Content-Type' always;
if ($request_method = OPTIONS) {
return 204;
}
}
触发预检的条件
满足任意一条就会触发 OPTIONS 预检:
- 请求方法不是 GET / POST / HEAD
- Content-Type 不是
text/plain/application/x-www-form-urlencoded/multipart/form-data - 有自定义请求头(
Authorization、X-Request-ID等)
误用三:Content-Type 漏掉 charset
错误写法
Content-Type: text/html
Content-Type: application/json
正确写法
Content-Type: text/html; charset=UTF-8
Content-Type: application/json; charset=UTF-8
Content-Type: text/plain; charset=UTF-8
浏览器遇到 text/html 没有 charset,会查 <meta charset="UTF-8"> 标签——但如果 HTML 本身就是乱码的,meta 标签也读不出来。服务端直接声明 charset 是最可靠的。
误用四:Vary 头缺失导致缓存不分语言
问题场景
API 根据 Accept-Language 返回中文或英文内容,但 CDN 缓存了第一个请求的内容,后来的请求不管什么语言都返回同一份。
原因
没有告诉 CDN “这个响应因 Accept-Language 而变化”。
修复
Vary: Accept-Language
或者同时因多个头变化:
Vary: Accept-Encoding, Accept-Language
Vary 告诉代理/CDN:缓存时把 Vary 里列出的请求头作为缓存 key 的一部分。没有 Vary,代理只用 URL 做 key——同 URL 不同语言的用户拿到同一份缓存。
误用五:安全头遗漏或配置过松
最基础的安全头组合
# 防止点击劫持(用 iframe 嵌套你的页面)
X-Frame-Options: DENY
# 防止 MIME 嗅探(浏览器猜 Content-Type)
X-Content-Type-Options: nosniff
# 强制 HTTPS(需配合 HSTS preload 才能保护首次访问)
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
# 内容安全策略(防 XSS)——先用 report-only 观察,再上 enforcement
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{随机值}'
# 控制 Referer 信息泄露
Referrer-Policy: strict-origin-when-cross-origin
快速验证
# 用 curl 查看响应头
curl -I https://yourdomain.com
# 或用在线工具 securityheaders.com 扫描评分