安全响应头(HTTP Security Headers)是浏览器在加载页面时最先执行的一道防线。对Node.js项目来说,与其手动在响应里逐个设置这些头,不如直接使用helmet这个中间件,它把十几个安全相关的响应头封装成了一站式配置,覆盖了内容安全策略、传输安全、点击劫持防护等常见场景。本文重点讲解其中最核心的两个配置:CSP内容安全策略和HSTS严格传输安全。

一、为什么需要安全头,helmet做了什么
安全头的工作原理很简单:服务器在响应中带上特定的头字段,浏览器读到后会主动限制某些行为。比如Content-Security-Policy告诉浏览器只允许加载白名单里的资源,Strict-Transport-Security强制后续请求走HTTPS,X-Frame-Options阻止页面被恶意网站嵌入iframe。这些防护不需要改动任何业务逻辑,只需要在响应头层面做声明。
helmet做的事情就是把这些头的设置收敛到一个中间件里。安装很简单:
npm install helmet
然后在Express应用中启用:
const express = require('express');
const helmet = require('helmet');
const app = express();
// 一行代码启用全部默认安全头
app.use(helmet());
app.get('/', (req, res) => {
res.send('hello');
});
app.listen(3000);调用helmet()之后,默认会启用X-Frame-Options、X-Content-Type-Options、Referrer-Policy等一系列头。但要注意,CSP在helmet 4.x之后默认只设置了default-src 'self',HSTS默认的max-age只有15552000秒(180天),这两个往往需要根据实际业务单独调整。
二、CSP内容安全策略的配置方法
CSP的核心是一组指令,每个指令控制一类资源的加载来源。default-src是兜底指令,其他指令没有显式声明时会回退到它。常用指令包括script-src(脚本)、style-src(样式)、img-src(图片)、connect-src(XHR和WebSocket目标)、font-src(字体)等。
在helmet中配置CSP的方式如下:
app.use(
helmet.contentSecurityPolicy({
useDefaults: true,
directives: {
'default-src': ["'self'"],
'script-src': ["'self'", 'https://cdn.example-cdn.com'],
'style-src': ["'self'", "'unsafe-inline'"],
'img-src': ["'self'", 'data:', 'https:'],
'connect-src': ["'self'", 'wss://api.bbccb.com'],
'object-src': ["'none'"],
'upgrade-insecure-requests': []
}
})
);这段配置的含义是:脚本只能从自身域名和一个指定的CDN加载;样式允许内联;图片允许HTTPS来源和data URI;XHR和WebSocket只能连自身以及指定的接口域名;object-src 'none'则彻底禁止插件内容。
配置CSP最常见的坑是内联脚本被拦截。如果页面里直接写了<script>标签或者依赖onclick这类内联事件,严格CSP会直接把它们拦下来,控制台会报错。解决思路有三种:给内联脚本加nonce,改造成外链文件,或者临时使用'unsafe-inline'(不推荐,等于放弃了CSP对XSS的防护价值)。nonce方式的写法是在每个响应里生成随机串,同时注入到页面脚本标签和响应头中:
app.use((req, res, next) => {
res.locals.nonce = crypto.randomUUID();
res.setHeader(
'Content-Security-Policy',
`script-src 'self' 'nonce-${res.locals.nonce}'`
);
next();
});
// 模板中输出: <script nonce="...">...</script>建议先用Report-Only模式观察一段时间,helmet 7.x提供了helmet.contentSecurityPolicy配合reportOnly选项,或者手动设置Content-Security-Policy-Report-Only头,把线上真实的违规情况收集起来再收紧策略,避免上线即白屏。
三、HSTS的配置与注意事项
HSTS的作用是告诉浏览器:在接下来的一段时间内,访问这个域名必须使用HTTPS,即使用户手动输入http://,浏览器也会在本地直接升级为HTTPS,从而防止SSL剥离攻击。它只对通过HTTPS访问的响应生效,HTTP响应里设置这个头会被浏览器忽略。
helmet中的HSTS由strictTransportSecurity模块负责:
app.use(
helmet.hsts({
maxAge: 31536000, // 一年,单位秒
includeSubDomains: true, // 覆盖所有子域名
preload: false
})
);maxAge建议设置为31536000(一年),这是目前的主流标准。includeSubDomains要谨慎开启:它会强制所有子域名走HTTPS,如果你还有某个子域名只支持HTTP(比如内部系统或者老旧服务),开启后用户将无法访问。确认全部子域名都支持HTTPS后再打开。preload表示愿意加入浏览器厂商维护的HSTS预加载列表,这是不可逆的操作,提交前需要确保整站包括所有子域名的HTTPS都稳定可用,一般建议最后再考虑。
另外一个配合点是把HTTP流量301重定向到HTTPS。HSTS只在浏览器第一次成功访问HTTPS之后才生效,重定向兜住了首次访问的场景:
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
return res.redirect(301, `https://${req.hostname}${req.url}`);
}
next();
});如果Node.js服务跑在Nginx或负载均衡后面,记得信任代理设置(app.set('trust proxy', 1)),否则x-forwarded-proto取值会不准确。
四、生产环境推荐配置与实践建议
把前面两部分整合起来,一个比较稳妥的生产环境写法如下:
const express = require('express');
const helmet = require('helmet');
const app = express();
app.use(
helmet({
contentSecurityPolicy: {
useDefaults: true,
directives: {
'default-src': ["'self'"],
'script-src': ["'self'"],
'img-src': ["'self'", 'data:'],
'upgrade-insecure-requests': []
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true
}
})
);上线后要验证配置是否生效。可以用curl -I查看响应头,确认Content-Security-Policy和Strict-Transport-Security字段的内容符合预期,也可以借助浏览器开发者工具的Network面板逐个检查请求。
最后强调几点:CSP策略不是一次配好就结束的,业务引入新的第三方资源时要同步更新白名单;HSTS的maxAge一旦被浏览器记住,回滚周期等于设置的时长,所以首次上线可以先用较短时间试运行;nonce方案比'unsafe-inline'安全得多,值得花时间改造模板。安全头配置好之后,配合定期的安全扫描,能以极低的维护成本持续为站点兜底。