Helmet 配置方法与 Express 5 实战
1. 安装与最小使用
npm install helmet
Helmet 自带 TypeScript 类型声明,现代版本通常不需要额外安装 @types/helmet。
import express from "express";
import helmet from "helmet";
const app = express();
const port = 8080;
app.use(helmet());
app.get("/health", (_req, res) => {
res.json({ ok: true });
});
app.listen(port, () => {
console.log(`server listening on http://127.0.0.1:${port}`);
});
本项目用 TypeScript 编译为 CommonJS,但源码仍可使用 import。若项目没有开启 esModuleInterop,默认导入是否可用取决于当前 TypeScript 配置和依赖导出方式;应以项目现有 tsconfig.json 的类型检查结果为准。
验证:
curl -i http://127.0.0.1:8080/health
2. 配置对象
可以保留有学习价值的显式类型:
import helmet, { type HelmetOptions } from "helmet";
const helmetOptions: HelmetOptions = {
crossOriginResourcePolicy: {
policy: "same-site",
},
referrerPolicy: {
policy: "strict-origin-when-cross-origin",
},
};
app.use(helmet(helmetOptions));
显式类型能让编辑器提示可用字段并尽早发现拼写错误。若类型名随依赖版本变化,以安装版本导出的类型为准;也可以让 TypeScript 从 helmet({...}) 参数推断。
3. 关闭单项策略
如果确认某项默认策略与业务冲突,可以只关闭这一项:
app.use(
helmet({
contentSecurityPolicy: false,
}),
);
这不应成为“页面报错就关闭 Helmet”的固定做法。正确顺序是:
- 查看浏览器 Console 的具体违规;
- 确认哪个响应携带策略;
- 补充最小必要来源或调整单项策略;
- 记录关闭原因并安排复核。
4. 配置 CSP
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "https://cdn.example.com"],
imgSrc: ["'self'", "data:", "https://images.example.com"],
connectSrc: ["'self'", "https://api.example.com"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
},
},
}),
);
注意字符串中的单引号是 CSP 语法的一部分,"'self'" 与普通字符串 "self" 不相同。
Nonce 示例
有内联脚本需求时,不要轻率加入 'unsafe-inline'。可以为每个 HTML 响应生成随机 nonce,并在 CSP 与 <script nonce="..."> 中使用同一个值。
import { randomBytes } from "node:crypto";
import type { NextFunction, Request, Response } from "express";
function createCspNonce(
_req: Request,
res: Response,
next: NextFunction,
): void {
res.locals.cspNonce = randomBytes(16).toString("base64");
next();
}
app.use(createCspNonce);
app.use(
helmet({
contentSecurityPolicy: {
directives: {
scriptSrc: [
"'self'",
(_req, res) => `'nonce-${String(res.locals.cspNonce)}'`,
],
},
},
}),
);
res.locals 随当前响应生命周期使用;请求结束且没有其他长期引用时,数据即可被垃圾回收。模板渲染时还要把相同 nonce 放到对应脚本标签。
5. 开发环境处理 upgrade-insecure-requests
本地使用 HTTP 时,CSP 的 upgrade-insecure-requests 可能让某些浏览器把本地资源升级为 HTTPS。可以按环境调整 directive:
const isDevelopment = process.env.NODE_ENV === "development";
app.use(
helmet({
contentSecurityPolicy: {
directives: {
"upgrade-insecure-requests": isDevelopment ? null : [],
},
},
}),
);
null 的具体配置语义应与当前 Helmet 版本文档和类型声明核对。更重要的是测试环境要尽量接近生产 HTTPS,避免上线时才第一次执行真实策略。
6. 单独调用某个 Helmet 中间件
Helmet 暴露了单项中间件,适合精确控制:
app.use(helmet.noSniff());
app.use(helmet.frameguard({ action: "sameorigin" }));
app.use(
helmet.referrerPolicy({
policy: "strict-origin-when-cross-origin",
}),
);
通常优先使用 helmet() 建立完整基线,再按业务调整;只零散启用两三个 Header 容易遗漏保护。
7. 仅对部分路由应用
app.use("/preview", helmet());
app.get("/api/health", (_req, res) => {
res.json({ ok: true });
});
也可以给 HTML 预览和 JSON API 使用不同策略:
const apiHelmet = helmet({
contentSecurityPolicy: false,
});
const previewHelmet = helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'none'"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
},
},
});
app.use("/api", apiHelmet);
app.use("/preview", previewHelmet);
这种拆分比“所有响应一套 CSP”更符合不同内容的风险,但不要造成配置散落。建议集中在一个安全配置模块,并说明每项差异。
8. 中间件顺序
一个易理解的基本顺序:
app.use(requestIdMiddleware);
app.use(helmet());
app.use(corsMiddleware);
app.use(express.json());
app.use(authenticationMiddleware);
app.use(apiRouter);
app.use(notFoundHandler);
app.use(errorHandler);
Helmet 和 CORS 较早执行,能让正常、404 和业务错误响应都带上相应 Header。若某个中间件在它们之前直接结束响应,那份响应不会自动得到后面的 Header。
不要多次调用 helmet() 后期待配置自动“合并”。多个中间件可能覆盖或重复 Header,应明确最终责任。
9. HSTS 在 Nginx HTTPS 架构中的选择
当公网 HTTPS 在 Nginx 终止、Express 只监听 127.0.0.1:8080 时,HSTS 通常由 Nginx 设置更清晰。此时可以在 Express 中关闭该项:
app.use(
helmet({
strictTransportSecurity: false,
}),
);
但关闭的前提是确认所有公网 HTTPS 响应(包括错误页)都由 Nginx正确添加 HSTS。不要让两层都以不同 max-age 或 includeSubDomains 设置。
10. 升级和测试建议
至少验证这些响应:
- JSON 成功响应;
- 参数错误、未登录、无权限;
- 404 与全局错误;
- HTML 页面或预览;
- 下载和图片;
- OPTIONS 预检;
- Nginx 公网地址,而不仅是 Express 内网端口。
升级 Helmet 后运行自动化测试,对关键 Header 做断言,但只断言业务真正依赖的策略,避免把所有默认字符串写死导致正常升级困难。