
安全标头:您的最后一道防线
安全标头并不能防止代码中的漏洞——它们限制了攻击者在漏洞存在时所能做的事情。它们是浏览器级别的安全网,能够捕获穿透应用程序防御的攻击。
好消息是:大多数安全标头只需一行代码即可实现。挑战在于理解每个标头的作用、哪种值适合您的应用程序,以及如何测试它们是否真正生效。

Content-Security-Policy (CSP)
最强大且最复杂的安全标头。CSP 为浏览器可以加载的每种资源类型定义了一个白名单:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com 'nonce-{random}';
style-src 'self' https://fonts.googleapis.com 'unsafe-inline';
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https:;
connect-src 'self' https://api.example.com wss://ws.example.com;
media-src 'none';
object-src 'none';
frame-src https://youtube.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
report-uri https://example.com/csp-reports;
关键指令说明:
| 指令 | 控制内容 |
|---|---|
default-src |
所有未指定资源类型的回退 |
script-src |
JavaScript 来源(最重要!) |
style-src |
CSS 来源 |
img-src |
图片来源 |
connect-src |
fetch、XHR、WebSocket 目标 |
frame-ancestors |
谁可以嵌入此页面(替代 X-Frame-Options) |
base-uri |
限制 <base> 标签(防止 base 标签注入) |
form-action |
表单可以提交到哪里 |
upgrade-insecure-requests |
将 HTTP 子资源升级为 HTTPS |
从仅报告模式开始:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reports
这会在不阻止任何内容的情况下报告违规——对于在部署前测试 CSP 至关重要。
CSP Nonces(优于 unsafe-inline)
// Express 中间件,每个请求生成新的 nonce
import crypto from 'crypto'
export function cspMiddleware(req: Request, res: Response, next: NextFunction) {
const nonce = crypto.randomBytes(16).toString('base64')
res.locals.nonce = nonce
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}'`,
`style-src 'self' 'nonce-${nonce}'`,
"img-src 'self' data: https:",
"object-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'",
].join('; '))
next()
}
// 在模板中:
// <script nonce="<%= nonce %>">...</script>
HTTP Strict Transport Security (HSTS)
强制浏览器始终使用 HTTPS 访问您的域名:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
max-age=31536000:浏览器记住仅 HTTPS 持续 1 年includeSubDomains:应用于所有子域名preload:允许提交到浏览器预加载列表(硬编码 HTTPS)
部署策略——从保守开始,逐步增加:
# 阶段 1:使用短 max-age 测试
Strict-Transport-Security: max-age=300
# 阶段 2:增加到 1 天
Strict-Transport-Security: max-age=86400
# 阶段 3:添加子域名(仅当所有子域名都支持 HTTPS 时)
Strict-Transport-Security: max-age=86400; includeSubDomains
# 阶段 4:全年(提交到预加载列表)
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
警告:带有 preload 的 HSTS 很难撤销。如果您无法从子域名提供 HTTPS,请不要使用 includeSubDomains。

CORS(跨域资源共享)
CORS 控制哪些域名可以从浏览器向您的 API 发出请求:
// Express CORS 中间件
import cors from 'cors'
// ❌ 对于生产 API 过于宽松:
app.use(cors()) // 允许所有来源
// ✅ 显式白名单:
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null,
].filter(Boolean) as string[]
app.use(cors({
origin: (origin, callback) => {
// 允许没有 origin 的请求(curl、Postman、服务器到服务器)
if (!origin) return callback(null, true)
if (allowedOrigins.includes(origin)) {
callback(null, true)
} else {
callback(new Error(`CORS blocked: ${origin} not allowed`))
}
},
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token'],
credentials: true, // 允许 Cookie
maxAge: 86400, // 缓存预检请求 24 小时
}))
理解预检请求:
对于“复杂”请求(非简单方法、自定义标头):
浏览器首先发送 OPTIONS 请求:
OPTIONS /api/users HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
服务器响应:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, GET, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
然后浏览器发送实际请求。
X-Frame-Options 与 frame-ancestors
# 旧方式——IE11 仍然需要:
X-Frame-Options: DENY
# 或:
X-Frame-Options: SAMEORIGIN
# 现代方式(CSP frame-ancestors):
Content-Security-Policy: frame-ancestors 'none'
# 或:
Content-Security-Policy: frame-ancestors 'self' https://trusted-dashboard.example.com
为了最大浏览器兼容性,同时使用两者:
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'
Permissions-Policy
控制页面和 iframe 的浏览器功能:
Permissions-Policy:
camera=(),
microphone=(),
geolocation=(self),
payment=(self "https://payment.example.com"),
usb=(),
accelerometer=(),
gyroscope=()
camera=():完全禁用(空 = 拒绝所有)geolocation=(self):仅允许此来源payment=(self "https://..."):允许此来源 + 特定第三方

其他安全标头
# 防止 MIME 类型嗅探
X-Content-Type-Options: nosniff
# 控制 Referrer 信息
Referrer-Policy: strict-origin-when-cross-origin
# strict-origin-when-cross-origin:跨域 HTTPS→HTTPS 仅发送来源,
# 同域发送完整 URL,HTTPS→HTTP 不发送
# 跨域策略(高级)
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
# 这三个一起启用 SharedArrayBuffer 和高精度计时器
# 需要:WebAssembly 线程、Atomics、performance.measureUserAgentSpecificMemory()
完整标头配置
Express.js 配合 Helmet:
import helmet from 'helmet'
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
frameSrc: ["'none'"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
permittedCrossDomainPolicies: false,
crossOriginEmbedderPolicy: false, // 仅在需要时启用
}))
Next.js(next.config.js):
const securityHeaders = [
{ key: 'X-DNS-Prefetch-Control', value: 'on' },
{ key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains; preload' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=(self)' },
]
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: securityHeaders,
},
]
},
}
测试安全标头
# 使用 curl 检查标头:
curl -I https://example.com
# 或检查特定标头:
curl -sI https://example.com | grep -i "content-security-policy"
# 在线工具:
# https://securityheaders.com — 为您的标头评分
# https://observatory.mozilla.org — 全面安全分析
# https://csp-evaluator.withgoogle.com — CSP 特定评估
# 使用仅报告 + 日志在本地测试 CSP:
# 设置:Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
# 添加路由记录违规:
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
console.log('CSP Violation:', JSON.stringify(req.body, null, 2))
res.status(204).send()
})
安全标头添加成本低廉,并能有效减少攻击面。实施它们所需的成本——几行配置——远低于处理一次成功的 XSS 攻击,而适当的 CSP 本可以阻止它。
→ 使用 HTTP 状态码 参考查找 HTTP 状态码及其含义。