HTTP 安全头是 Web 应用安全的第一道防线。它们通过 HTTP 响应头指示浏览器执行特定的安全策略——限制跨域请求、防止 XSS 注入、强制 HTTPS、禁止点击劫持。一个正确的安全头配置可以大幅降低攻击面,但配置不当也可能导致功能异常或给用户带来安全风险。
本文从工程师视角系统剖析每一个重要的安全头,覆盖从配置到调试到渐进部署的完整实践。
一、CORS:跨源资源共享
1.1 同源策略与 CORS 的关系
同源策略(Same-Origin Policy)是浏览器的基本安全模型。它限制了一个源(协议 + 域名 + 端口)的文档或脚本与另一个源的资源交互。
同源判定示例(基于 https://api.example.com:443):
https://api.example.com/path → ✅ 同源
http://api.example.com/path → ❌ 协议不同(https vs http)
https://www.example.com/path → ❌ 域名不同(api vs www)
https://api.example.com:8443 → ❌ 端口不同(443 vs 8443)
CORS(Cross-Origin Resource Sharing,跨源资源共享)是一种机制,允许服务端声明”哪些源可以访问我的资源”。
1.2 简单请求 vs 预检请求
CORS 将请求分为两类:
简单请求(Simple Request)— 直接发送,不触发预检:
条件:
1. 方法: GET / HEAD / POST
2. 头字段: 仅限 Accept, Accept-Language, Content-Language,
Content-Type (限 text/plain, multipart/form-data, application/x-www-form-urlencoded)
3. 没有 ReadableStream body
4. 没有事件监听器
预检请求(Preflight Request)— 先发 OPTIONS,再发实际请求:
触发条件(任一):
1. 方法: PUT / DELETE / PATCH 等
2. 头字段: Authorization, Content-Type: application/json 等
3. 自定义头: X-Custom-Header
1.3 预检请求的完整流程
# 步骤 1: 浏览器自动发送 OPTIONS 预检请求
OPTIONS /api/users HTTP/2
Host: api.example.com
Origin: https://www.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
# 步骤 2: 服务端返回 CORS 策略
HTTP/2 204 No Content
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
# 预检结果缓存 24 小时,期间不再发送 OPTIONS
# 步骤 3: 浏览器发送实际请求
POST /api/users HTTP/2
Host: api.example.com
Origin: https://www.example.com
Content-Type: application/json
Authorization: Bearer eyJhbG...
# 步骤 4: 服务端响应(必须包含 CORS 头)
HTTP/2 201 Created
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Expose-Headers: X-Request-Id
# 如果响应中没有 Access-Control-Allow-Origin,浏览器会丢弃响应
1.4 CORS 头字段完整指南
# ── 响应头字段 ──
# 允许的源(必须精确匹配,或使用 *)
Access-Control-Allow-Origin: https://www.example.com
# 或
Access-Control-Allow-Origin: *
# 注意:使用 * 时不能携带 Cookie(credentials)
# 允许的 HTTP 方法
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH
# 允许的请求头
Access-Control-Allow-Headers: Content-Type, Authorization, X-Request-Id
# 允许携带凭证(Cookie、Authorization 头)
Access-Control-Allow-Credentials: true
# 注意:使用 Credentials 时,Allow-Origin 不能是 *
# 暴露给 JavaScript 的响应头(默认只暴露 Cache-Control 等安全头)
Access-Control-Expose-Headers: X-Request-Id, X-RateLimit-Remaining
# 预检结果缓存时间
Access-Control-Max-Age: 86400
# Chrome 最大 7200 秒(2 小时),Firefox 最大 86400 秒
1.5 Nginx CORS 配置
# /etc/nginx/conf.d/cors.conf
map $http_origin $cors_origin {
default "";
"~^https://www\.example\.com$" $http_origin;
"~^https://app\.example\.com$" $http_origin;
"~^https://admin\.example\.com$" $http_origin;
}
server {
location /api/ {
# 设置 CORS 头
if ($cors_origin != "") {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Request-Id" always;
add_header Access-Control-Expose-Headers "X-Request-Id, X-RateLimit-Remaining" always;
add_header Access-Control-Max-Age 7200 always;
}
# 处理 OPTIONS 预检
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://backend;
}
}
1.6 CORS 的性能影响
预检请求(OPTIONS)会增加一个额外的 RTT:
没有预检:
[Request] ──→ [Response] 总计: 1 RTT
有预检:
[OPTIONS] ──→ [204] 预检: 1 RTT
[Request] ──→ [Response] 实际: 1 RTT
总计: 2 RTT
缓解策略:
1. Access-Control-Max-Age: 7200(缓存预检结果 2 小时)
2. 尽可能使用简单请求(避免触发预检)
- 使用 application/x-www-form-urlencoded 而非 application/json
- 但这会牺牲 API 设计的清晰性
3. 同源部署(同域名下的 API 不需要 CORS)
- /api/* → 后端服务
- / → 前端静态资源
1.7 CORS 常见错误
❌ 错误 1: Access-Control-Allow-Origin: * + Credentials: true
浏览器报错: "Cannot use wildcard with credentials"
修复: 使用具体的 Origin 值
❌ 错误 2: 只在 200 响应中返回 CORS 头
结果: 4xx/5xx 错误时浏览器无法读取错误信息
修复: add_header ... always(Nginx 中 always 表示所有状态码)
❌ 错误 3: 遗漏 Vary: Origin
结果: CDN 缓存了一个 Origin 的 CORS 头,返回给另一个 Origin
修复: 动态 Origin 时必须添加 Vary: Origin
❌ 错误 4: 忘记处理 OPTIONS 方法
结果: 预检请求被后端框架作为普通请求处理,返回 405
修复: 在代理层返回 204,或在框架中注册 OPTIONS 路由
二、CSP:内容安全策略
2.1 CSP 基础
内容安全策略(Content Security Policy)通过白名单机制控制页面可以加载哪些资源:
# CSP 通过 HTTP 头或 <meta> 标签设置
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com
# 含义:
# default-src 'self': 默认只允许加载同源资源
# script-src 'self' https://cdn.example.com: 脚本可以从同源或 CDN 加载
# 其他未列出的资源类型继承 default-src 的规则
2.2 CSP 指令全表
# 资源加载指令
default-src # 所有资源的默认策略
script-src # JavaScript 脚本
style-src # CSS 样式表
img-src # 图片
font-src # 字体文件
connect-src # XHR/fetch/WebSocket 连接目标
media-src # <video>/<audio> 媒体
object-src # <object>/<embed>/<applet>(建议设为 'none')
frame-src # <iframe> 嵌入的页面
child-src # Web Worker 和嵌入的浏览上下文
worker-src # Web Worker / Service Worker / Shared Worker
manifest-src # Web App Manifest
# 文档指令
base-uri # <base> 标签的 URL(建议 'self')
form-action # <form> 的提交目标
frame-ancestors # 谁可以嵌入此页面(替代 X-Frame-Options)
# 导航指令
navigate-to # 页面可以导航到的 URL
# 报告指令
report-uri # CSP 违规报告的目标 URL(已废弃)
report-to # CSP 违规报告的目标(使用 Reporting API)
2.3 CSP 的源列表语法
# 关键字值(必须带引号)
'self' # 同源
'none' # 禁止所有
'unsafe-inline' # 允许内联脚本/样式(不推荐!)
'unsafe-eval' # 允许 eval()(不推荐!)
'strict-dynamic' # 信任由受信任脚本动态加载的脚本
'nonce-{random}' # 允许特定 nonce 的内联脚本
# URL 匹配
https://cdn.example.com # 精确域名
https://*.example.com # 通配符子域名
https: # 所有 HTTPS URL
data: # data: URI
blob: # blob: URI
# 哈希值(允许特定内容的内联脚本/样式)
'sha256-{base64-hash}'
2.4 CSP 渐进部署
直接部署严格的 CSP 通常会导致功能异常。推荐的渐进路径:
# 阶段 1: Report-Only(只报告不阻止,观察几周)
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
report-to csp-endpoint
# 配置报告端点
Reporting-Endpoints: csp-endpoint="https://report.example.com/csp"
# 阶段 2: 分析报告,修复违规
# 常见违规:
# - 内联脚本被阻止 → 移到外部文件或使用 nonce
# - 第三方资源被阻止 → 加入白名单
# - eval() 被阻止 → 替换为安全的替代方案
# 阶段 3: 部署强制模式
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
# 阶段 4: 进一步收紧
# 使用 nonce 替换 'unsafe-inline'
# 移除不必要的第三方域名
2.5 Nonce-based CSP
<!-- 服务端为每个请求生成唯一的 nonce -->
<!-- HTTP 头: Content-Security-Policy: script-src 'nonce-abc123random' -->
<!-- 带 nonce 的内联脚本会被执行 -->
<script nonce="abc123random">
console.log('This script is allowed');
</script>
<!-- 没有 nonce 或 nonce 不匹配的内联脚本会被阻止 -->
<script>
console.log('This script is BLOCKED by CSP');
</script>
<!-- XSS 注入的脚本也会被阻止(攻击者不知道 nonce) -->
<script>steal(document.cookie)</script> <!-- BLOCKED -->// Go: 生成 CSP nonce
package main
import (
"crypto/rand"
"encoding/base64"
"fmt"
"net/http"
)
func generateNonce() string {
b := make([]byte, 16)
rand.Read(b)
return base64.StdEncoding.EncodeToString(b)
}
func handler(w http.ResponseWriter, r *http.Request) {
nonce := generateNonce()
csp := fmt.Sprintf("script-src 'nonce-%s' 'strict-dynamic'", nonce)
w.Header().Set("Content-Security-Policy", csp)
html := fmt.Sprintf(`<html>
<script nonce="%s">console.log("allowed");</script>
</html>`, nonce)
w.Write([]byte(html))
}2.6 strict-dynamic
strict-dynamic 是 CSP Level 3
引入的关键特性。它允许受信任的脚本动态加载其他脚本,而不需要预先白名单所有
CDN URL:
# 传统方式:需要列出所有第三方脚本域名
Content-Security-Policy: script-src 'self'
https://cdn.example.com
https://analytics.google.com
https://widget.intercom.io
https://js.stripe.com
...
# 维护噩梦:每增加一个第三方脚本都要更新 CSP
# strict-dynamic 方式:
Content-Security-Policy: script-src 'nonce-{random}' 'strict-dynamic'
# 含义:
# 1. 只有带正确 nonce 的脚本可以执行
# 2. 这些受信任脚本动态创建的 <script> 也被允许执行
# 3. URL 白名单被忽略(只看 nonce)
三、HSTS:HTTP 严格传输安全
3.1 HSTS 基础
HSTS(HTTP Strict Transport Security)告诉浏览器”在指定时间内,只通过 HTTPS 访问这个域名”:
# 基本 HSTS
Strict-Transport-Security: max-age=31536000
# 1 年内,浏览器自动将 http:// 转为 https://
# 不发送 HTTP 请求(内部 307 重定向)
# 包含子域名
Strict-Transport-Security: max-age=31536000; includeSubDomains
# 所有子域名也强制 HTTPS
# HSTS Preload
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
# 申请加入浏览器内置的 HSTS 预加载列表
3.2 HSTS 解决的问题
没有 HSTS 时的攻击场景(SSL Stripping):
用户输入 example.com → 浏览器发送 HTTP 请求
↓
中间人拦截 HTTP 请求
↓
中间人与 example.com 建立 HTTPS 连接
中间人与用户保持 HTTP 连接
↓
用户在 HTTP 上操作,所有数据被中间人窃取
有 HSTS 时:
用户输入 example.com → 浏览器检查 HSTS 列表
↓
发现 example.com 在 HSTS 列表中
↓
内部 307 重定向到 https://example.com
↓
直接发送 HTTPS 请求,没有 HTTP 请求可以被拦截
3.3 HSTS Preload
HSTS Preload 将域名硬编码到浏览器中,解决”首次访问”的问题:
# 申请 HSTS Preload:https://hstspreload.org
# 要求:
# 1. 有效的 HTTPS 证书
# 2. 将所有 HTTP 流量重定向到 HTTPS
# 3. 所有子域名也支持 HTTPS
# 4. HSTS 头包含:max-age >= 31536000; includeSubDomains; preload
# 注意:加入 Preload 列表后很难移除!
# 移除过程可能需要数月(等待浏览器更新)
# 确保所有子域名都支持 HTTPS 后再申请3.4 HSTS 的工程风险
风险 1: 子域名不支持 HTTPS
设置了 includeSubDomains,但 internal.example.com 没有证书
→ 用户无法访问 internal.example.com
修复: 确认所有子域名都有 HTTPS 证书后再启用
风险 2: max-age 过长导致回滚困难
设置了 max-age=2年,后来需要回退到 HTTP
→ 已访问过的用户 2 年内无法通过 HTTP 访问
建议: 从小的 max-age 开始(如 300 秒),逐步增加
风险 3: Preload 后无法回退
加入 Preload 列表后,全球所有浏览器都会强制 HTTPS
即使从服务端移除 HSTS 头,Preload 列表中的条目仍然生效
四、X-Frame-Options 与 frame-ancestors
4.1 点击劫持防御
# X-Frame-Options(旧标准,仍然广泛使用)
X-Frame-Options: DENY # 完全禁止被嵌入 iframe
X-Frame-Options: SAMEORIGIN # 只允许同源嵌入
# CSP frame-ancestors(新标准,更灵活)
Content-Security-Policy: frame-ancestors 'self' # 同源
Content-Security-Policy: frame-ancestors 'self' https://trusted.com # 同源 + 白名单
Content-Security-Policy: frame-ancestors 'none' # 禁止嵌入
# 推荐同时设置(向后兼容):
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'
4.2 点击劫持攻击原理
<!-- 攻击页面:将目标网站嵌入透明 iframe -->
<style>
iframe {
position: absolute;
opacity: 0; /* 透明,用户看不到 */
width: 500px;
height: 500px;
z-index: 10; /* 在按钮上方 */
}
.fake-button {
position: absolute;
/* 位置对齐到 iframe 中的"删除账户"按钮 */
}
</style>
<iframe src="https://target.com/settings"></iframe>
<button class="fake-button">点击领取奖品</button>
<!-- 用户以为在点"领取奖品",实际在点 target.com 的"删除账户" -->五、其他重要安全头
5.1 Referrer-Policy
# 控制 Referer 头泄露多少信息
Referrer-Policy: strict-origin-when-cross-origin
# 同源请求: 完整 URL
# 跨源请求 (HTTPS→HTTPS): 只发送 Origin
# 跨源请求 (HTTPS→HTTP): 不发送 Referer
# 可选值:
Referrer-Policy: no-referrer # 从不发送
Referrer-Policy: no-referrer-when-downgrade # HTTPS→HTTP 不发送(默认)
Referrer-Policy: origin # 只发送 Origin
Referrer-Policy: origin-when-cross-origin # 跨域只发 Origin,同域发完整 URL
Referrer-Policy: same-origin # 只对同源请求发送
Referrer-Policy: strict-origin # 只发 Origin,降级不发
Referrer-Policy: strict-origin-when-cross-origin # 推荐值
Referrer-Policy: unsafe-url # 始终发送完整 URL(不推荐)
5.2 Permissions-Policy
# 控制浏览器功能的使用(替代已废弃的 Feature-Policy)
Permissions-Policy:
camera=(), # 禁用摄像头
microphone=(), # 禁用麦克风
geolocation=(self), # 只允许同源使用地理位置
payment=(self "https://pay.example.com"), # 支付 API 白名单
autoplay=(self), # 自动播放限制
fullscreen=(self) # 全屏限制
# 禁用不需要的危险功能可以减少攻击面
# 即使 XSS 攻击者注入了使用这些功能的代码,浏览器也会拒绝
5.3 X-Content-Type-Options
# 防止 MIME 类型嗅探
X-Content-Type-Options: nosniff
# 问题场景:
# 攻击者上传一个名为 image.jpg 的文件,内容实际上是 JavaScript
# 没有 nosniff: 浏览器可能嗅探内容类型,将其作为 JS 执行
# 有 nosniff: 浏览器严格按照 Content-Type 处理,拒绝执行
# 这个头应该始终设置
5.4 X-DNS-Prefetch-Control
# 控制 DNS 预取行为
X-DNS-Prefetch-Control: off
# 防止浏览器自动预取页面中链接的 DNS
# 隐私考虑:DNS 预取可能泄露用户浏览行为
5.5 Cross-Origin-Opener-Policy(COOP)
# 控制窗口之间的跨源隔离
Cross-Origin-Opener-Policy: same-origin
# 可选值:
# unsafe-none — 默认行为,允许跨源窗口交互
# same-origin-allow-popups — 同源,但允许弹出窗口
# same-origin — 完全隔离,跨源窗口互相不可引用
COOP 的核心作用是防止 Spectre 类侧信道攻击。当设置
same-origin 时,跨源弹出窗口无法获取
window.opener
引用,攻击者无法通过共享的浏览上下文(browsing context
group)读取敏感数据。
5.6 Cross-Origin-Embedder-Policy(COEP)
# 要求所有子资源都明确允许被嵌入
Cross-Origin-Embedder-Policy: require-corp
# 可选值:
# unsafe-none — 默认行为
# require-corp — 所有跨源资源必须设置 CORP 头或 CORS 头
# credentialless — 跨源请求不携带凭证(较新,部分浏览器支持)
COOP + COEP 联合使用可启用
crossOriginIsolated 状态,解锁高精度 API:
// 检查是否处于跨源隔离状态
if (self.crossOriginIsolated) {
// 可使用 SharedArrayBuffer、高精度 performance.now()
console.log("Cross-origin isolated: SharedArrayBuffer available");
}5.7 安全头对照表
头字段 │ 防御目标 │ 推荐值
──────────────────────────────┼───────────────────────┼──────────────────────────
Strict-Transport-Security │ SSL 剥离 │ max-age=31536000; includeSubDomains
Content-Security-Policy │ XSS/数据注入 │ default-src 'self'; script-src 'nonce-…'
X-Frame-Options │ 点击劫持 │ SAMEORIGIN
X-Content-Type-Options │ MIME 嗅探 │ nosniff
Referrer-Policy │ 信息泄露 │ strict-origin-when-cross-origin
Permissions-Policy │ 功能滥用 │ camera=(), microphone=()
Cross-Origin-Opener-Policy │ Spectre 侧信道 │ same-origin
Cross-Origin-Embedder-Policy │ 跨源数据泄露 │ require-corp
六、安全头的完整配置模板
6.1 Nginx 安全头配置
# /etc/nginx/conf.d/security-headers.conf
# HSTS(确认所有子域名支持 HTTPS 后启用 includeSubDomains)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# CSP(根据实际需求调整)
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.example.com; frame-ancestors 'self'; base-uri 'self'; form-action 'self'" always;
# 点击劫持防御
add_header X-Frame-Options "SAMEORIGIN" always;
# MIME 类型嗅探防御
add_header X-Content-Type-Options "nosniff" always;
# Referrer 策略
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# 权限策略
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(self)" always;
6.2 安全头审计
#!/bin/bash
# security_headers_audit.sh - HTTP 安全头审计脚本
URL="${1:-https://example.com}"
echo "=== Security Headers Audit: $URL ==="
echo ""
headers=$(curl -sI "$URL")
check_header() {
local name="$1"
local value=$(echo "$headers" | grep -i "^${name}:" | head -1)
if [ -n "$value" ]; then
echo "✅ $name: $(echo "$value" | cut -d: -f2- | xargs)"
else
echo "❌ $name: Missing"
fi
}
check_header "Strict-Transport-Security"
check_header "Content-Security-Policy"
check_header "X-Frame-Options"
check_header "X-Content-Type-Options"
check_header "Referrer-Policy"
check_header "Permissions-Policy"
echo ""
echo "--- Additional Checks ---"
# 检查是否有不安全的头
if echo "$headers" | grep -qi "X-Powered-By"; then
echo "⚠️ X-Powered-By header present (information disclosure)"
fi
if echo "$headers" | grep -qi "Server:"; then
server=$(echo "$headers" | grep -i "^Server:" | head -1)
echo "⚠️ $server (consider hiding version)"
fi# 使用在线工具检查
# securityheaders.com
# observatory.mozilla.org
curl -s "https://securityheaders.com/?q=https://example.com&followRedirects=on" | \
grep -o 'class="[A-F]"' | head -1七、安全头部署的常见陷阱
7.1 CSP 破坏了内联脚本
问题: 部署 CSP 后,Google Analytics、第三方聊天组件等内联脚本停止工作
解决方案(按优先级):
1. 将内联脚本移到外部文件 → script-src 'self'
2. 使用 nonce → script-src 'nonce-{random}'
3. 使用哈希 → script-src 'sha256-{hash}'
4. 最后手段: 'unsafe-inline'(不推荐,但比没有 CSP 好)
7.2 HSTS 影响开发环境
问题: 在开发环境设置了 HSTS,但开发环境使用自签名证书或 HTTP
结果: 浏览器拒绝通过 HTTP 访问开发环境
解决方案:
1. 不在开发环境设置 HSTS
2. 使用 mkcert 生成本地信任的开发证书
3. Chrome: chrome://net-internals/#hsts → 删除域名的 HSTS 记录
7.3 CORS + 缓存不一致
问题: CDN 缓存了一个 Origin 的 CORS 响应头,返回给另一个 Origin
场景:
1. www.example.com 请求 → CDN 缓存 Access-Control-Allow-Origin: https://www.example.com
2. app.example.com 请求 → CDN 返回缓存的 Access-Control-Allow-Origin: https://www.example.com
3. 浏览器拒绝: Origin https://app.example.com 不匹配
解决方案:
Vary: Origin
告诉 CDN 按 Origin 头值区分缓存
7.4 Permissions-Policy 语法变更
问题: Feature-Policy 已废弃,旧配置不生效
旧语法(Feature-Policy):
Feature-Policy: camera 'none'; microphone 'none'
新语法(Permissions-Policy):
Permissions-Policy: camera=(), microphone=()
差异:
- 分隔符从分号变为逗号
- 值用括号 () 而非引号 'none'
- self 不加引号: geolocation=(self)
- 指定源: payment=(self "https://pay.example.com")
两个头可以同时设置以覆盖旧浏览器
7.5 安全头与 SPA 框架的冲突
问题: React/Vue 等 SPA 框架的开发模式依赖 eval() 和内联脚本
常见报错:
Refused to evaluate a string as JavaScript because 'unsafe-eval'
is not an allowed source of script in CSP
开发环境解决方案:
不在开发环境启用严格 CSP
或仅在生产构建时注入 CSP 头
生产环境方案:
- 避免使用依赖 eval() 的库(如部分模板引擎)
- 使用 nonce + strict-dynamic 而不是 'unsafe-eval'
- Webpack: 设置 devtool: 'source-map' 而非 'eval-source-map'
- Vue: 使用运行时构建(不需要模板编译器)
7.6 多层代理的安全头覆盖
问题: CDN / 反向代理 / 应用三层都设置了安全头,最终结果不可预测
典型场景:
应用层: Content-Security-Policy: default-src 'self'
Nginx: add_header Content-Security-Policy "default-src 'self' https://cdn.com"
CDN: 自动添加 X-Frame-Options: SAMEORIGIN
结果: 可能出现重复头(两个 CSP 头),浏览器行为取决于实现
解决方案:
1. 明确安全头的"唯一设置点"(通常是应用层或 Nginx,不要两者都设)
2. Nginx 使用 proxy_hide_header 先移除上游头,再 add_header 设置
3. 定期用 curl -I 验证最终响应头
八、总结
HTTP 安全头的部署是一个”渐进增强”的过程,不是一次性的开关。核心建议:
HSTS 从小 max-age 开始(300 秒),确认没问题后逐步增加到 31536000 秒(1 年)。includeSubDomains 和 preload 需要格外谨慎。
CSP 从 Report-Only 开始。先观察报告数据,修复违规项,再切换到强制模式。使用 nonce + strict-dynamic 是目前最佳的 CSP 策略。
CORS 要精确配置。避免
Access-Control-Allow-Origin: *+ Credentials 的组合。动态 Origin 时必须设置Vary: Origin。安全头不是万能的。它们是”深度防御”的一层,不能替代输入验证、输出编码和安全的应用逻辑。XSS 的根本解决方案是不信任用户输入,安全头只是在应用防御失败时的兜底。
定期审计安全头配置。使用 securityheaders.com 或 Mozilla Observatory 检查你的配置。每次部署新的第三方脚本或更改域名结构后,都应该重新检查 CSP 和 CORS 配置。
COOP + COEP 按需启用。如果你的应用需要
SharedArrayBuffer或高精度计时器,配置Cross-Origin-Opener-Policy: same-origin和Cross-Origin-Embedder-Policy: require-corp,但要确保所有跨源资源都设置了正确的 CORS 或 CORP 头。安全头的设置点要统一。避免在应用层、反向代理和 CDN 三个层面重复设置同一个安全头,选择一个层作为安全头的唯一控制点。
参考文献
- MDN Web Docs: HTTP Headers - Security
- OWASP: Secure Headers Project
- W3C: Content Security Policy Level 3
- RFC 6797: HTTP Strict Transport Security (HSTS)
- W3C: Cross-Origin Resource Sharing (CORS)
上一篇:HTTP 压缩与传输编码:gzip、Brotli 与 zstd 工程选型
下一篇:HTTP 调试方法论:curl、DevTools 与 mitmproxy
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【网络工程】Cookie 与 Session 工程:安全属性与现代限制
系统剖析 Cookie 的安全属性(Domain/Path/Secure/HttpOnly/SameSite)、第三方 Cookie 的消亡与替代方案、Session 管理工程实践与 CSRF 防护的现代实践。
【网络工程】QUIC 生态与工程部署:从实验到生产
QUIC 已经不是实验性协议——HTTP/3 标准化后,CDN、浏览器和主流服务端框架都在推进 QUIC 支持。本文从工程视角对比主流 QUIC 库的成熟度和性能特征,讲解 CDN/负载均衡器的 QUIC 适配方案、从 TCP 迁移到 QUIC 的渐进路径、QUIC 调试工具链,以及生产环境的部署陷阱和性能调优实践。
【网络工程】eBPF 可编程网络:从包过滤到流量工程
eBPF 正在重新定义网络工程——从传统的 iptables/netfilter 规则堆砌,到可编程、可观测、高性能的网络数据平面。本文系统讲解 eBPF 网络程序类型(XDP/TC/Socket)、Map 数据结构、Cilium 的 eBPF 数据平面实现,以及 eBPF 在负载均衡、可观测性和网络安全中的工程实践。
【网络工程】可编程数据平面与 P4:软件定义转发
传统网络设备的转发逻辑固化在硬件中。P4 语言让交换机的转发管线可编程——你可以定义自己的包头解析、匹配规则和转发动作。本文从 P4 语言核心概念出发,讲解 Parser/Match-Action/Deparser 的编程模型、可编程交换机芯片(Tofino)的架构、P4 在数据中心和运营商网络中的应用案例,以及 P4 与 eBPF 的定位差异。