Appearance
Node.js 安全最佳实践
Node.js应用的安全性是开发过程中的重要考虑因素。本章详细介绍Node.js应用的安全最佳实践。
输入验证和净化
防止注入攻击
javascript
// 不安全的代码示例
function unsafeQuery(db, userInput) {
// SQL注入风险
return db.query(`SELECT * FROM users WHERE id = ${userInput}`);
}
// 安全的代码示例
function safeQuery(db, userId) {
// 使用参数化查询
return db.query('SELECT * FROM users WHERE id = ?', [userId]);
}
// 使用ORM防止注入
const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize('database', 'username', 'password');
const User = sequelize.define('User', {
name: DataTypes.STRING,
email: DataTypes.STRING
});
// 安全的查询方式
async function findUserByEmail(email) {
// ORM会自动处理参数化
const user = await User.findOne({
where: { email: email }
});
return user;
}
输入验证中间件
javascript
const express = require('express');
const validator = require('validator'); // npm install validator
const app = express();
// 通用输入验证中间件
function validateInput(req, res, next) {
// 验证邮箱
if (req.body.email && !validator.isEmail(req.body.email)) {
return res.status(400).json({ error: '无效的邮箱格式' });
}
// 验证URL
if (req.body.website && !validator.isURL(req.body.website)) {
return res.status(400).json({ error: '无效的URL格式' });
}
// 验证字符串长度
if (req.body.name && !validator.isLength(req.body.name, { min: 1, max: 50 })) {
return res.status(400).json({ error: '姓名长度必须在1-50字符之间' });
}
// 验证数字范围
if (req.body.age && !validator.isInt(req.body.age, { min: 0, max: 150 })) {
return res.status(400).json({ error: '年龄必须是0-150之间的整数' });
}
next();
}
// 使用验证中间件
app.post('/users', validateInput, (req, res) => {
res.json({ message: '用户创建成功' });
});
使用Joi进行输入验证
javascript
// npm install joi
const Joi = require('joi');
// 定义用户验证模式
const userSchema = Joi.object({
name: Joi.string().min(1).max(50).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(0).max(150),
password: Joi.string().min(8).max(100).required(),
role: Joi.string().valid('user', 'admin', 'moderator').default('user')
});
// 验证函数
function validateUser(userData) {
const { error, value } = userSchema.validate(userData);
if (error) {
throw new Error(`验证失败: ${error.details[0].message}`);
}
return value;
}
// 在Express路由中使用
app.post('/users', (req, res) => {
try {
const validData = validateUser(req.body);
// 处理有效数据
res.json({ message: '用户数据验证通过', data: validData });
} catch (err) {
res.status(400).json({ error: err.message });
}
});
身份验证和授权
JWT认证实现
javascript
// npm install jsonwebtoken bcryptjs
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
class AuthService {
constructor(secret, expiresIn = '24h') {
this.secret = secret;
this.expiresIn = expiresIn;
}
async generateToken(payload) {
return jwt.sign(payload, this.secret, { expiresIn: this.expiresIn });
}
verifyToken(token) {
try {
return jwt.verify(token, this.secret);
} catch (err) {
throw new Error('无效的令牌');
}
}
async hashPassword(password) {
const saltRounds = 12;
return await bcrypt.hash(password, saltRounds);
}
async verifyPassword(password, hashedPassword) {
return await bcrypt.compare(password, hashedPassword);
}
}
// JWT中间件
function authenticateToken(authService) {
return (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ error: '访问令牌缺失' });
}
try {
const decoded = authService.verifyToken(token);
req.user = decoded;
next();
} catch (err) {
return res.status(403).json({ error: '无效的令牌' });
}
};
}
// 角色检查中间件
function requireRole(requiredRole) {
return (req, res, next) => {
if (!req.user || req.user.role !== requiredRole) {
return res.status(403).json({ error: '权限不足' });
}
next();
};
}
会话管理
javascript
const session = require('express-session'); // npm install express-session
const MongoStore = require('connect-mongo'); // npm install connect-mongo
// 安全的会话配置
app.use(session({
secret: process.env.SESSION_SECRET || 'your-super-secret-key',
resave: false,
saveUninitialized: false,
name: 'sessionId', // 自定义cookie名称
cookie: {
secure: process.env.NODE_ENV === 'production', // 仅在HTTPS下传输
httpOnly: true, // 防止XSS攻击
maxAge: 24 * 60 * 60 * 1000, // 24小时
sameSite: 'strict' // 防止CSRF攻击
},
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI
})
}));
安全头部设置
使用Helmet中间件
javascript
const helmet = require('helmet'); // npm install helmet
// 应用安全头部
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'", 'cdnjs.cloudflare.com'],
scriptSrc: ["'self'", 'cdnjs.cloudflare.com'],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'", 'https://api.example.com']
},
},
hsts: {
maxAge: 31536000, // 一年
includeSubDomains: true,
preload: true
},
frameguard: {
action: 'deny' // 防止点击劫持
},
xssFilter: true,
noSniff: true,
referrerPolicy: {
policy: ['no-referrer', 'strict-origin-when-cross-origin']
}
}));
自定义安全头部
javascript
// 自定义安全中间件
function securityHeaders(req, res, next) {
// 防止MIME类型嗅探
res.setHeader('X-Content-Type-Options', 'nosniff');
// 防止XSS攻击
res.setHeader('X-XSS-Protection', '1; mode=block');
// 防止点击劫持
res.setHeader('X-Frame-Options', 'DENY');
// 内容安全策略
res.setHeader('Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
);
// 强制HTTPS
res.setHeader('Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
// 防止引用来源泄漏
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
}
app.use(securityHeaders);
数据保护
敏感数据加密
javascript
const crypto = require('crypto');
class DataEncryption {
constructor(secretKey) {
this.algorithm = 'aes-256-cbc';
this.key = crypto.scryptSync(secretKey, 'GfG', 32);
}
encrypt(text) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipher(this.algorithm, this.key);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
decrypt(text) {
const parts = text.split(':');
const iv = Buffer.from(parts[0], 'hex');
const encrypted = parts[1];
const decipher = crypto.createDecipher(this.algorithm, this.key);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
// 使用示例
const encryptor = new DataEncryption('your-secret-key');
// 加密敏感数据
const encryptedEmail = encryptor.encrypt('user@example.com');
const decryptedEmail = encryptor.decrypt(encryptedEmail);
环境变量安全
javascript
// 使用dotenv安全管理环境变量
require('dotenv').config(); // npm install dotenv
// 安全配置检查
const config = {
port: process.env.PORT || 3000,
db: {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
name: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '24h'
},
session: {
secret: process.env.SESSION_SECRET
}
};
// 验证必需的环境变量
function validateConfig() {
const required = ['JWT_SECRET', 'SESSION_SECRET', 'DB_PASSWORD'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(`缺少必需的环境变量: ${missing.join(', ')}`);
}
}
validateConfig();
防止常见攻击
防止CSRF攻击
javascript
const csrf = require('csurf'); // npm install csurf
// CSRF保护中间件
const csrfProtection = csrf({
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production'
}
});
// 应用CSRF保护
app.use(csrfProtection);
// 提供CSRF令牌
app.use((req, res, next) => {
res.locals.csrfToken = req.csrfToken();
next();
});
// 需要CSRF保护的路由
app.post('/transfer', csrfProtection, (req, res) => {
// 处理转账请求
res.json({ message: '转账成功' });
});
防止XSS攻击
javascript
const xss = require('xss'); // npm install xss
// XSS净化中间件
function sanitizeInput(req, res, next) {
if (req.body) {
for (const key in req.body) {
if (typeof req.body[key] === 'string') {
req.body[key] = xss(req.body[key]);
}
}
}
if (req.query) {
for (const key in req.query) {
if (typeof req.query[key] === 'string') {
req.query[key] = xss(req.query[key]);
}
}
}
next();
}
app.use(sanitizeInput);
// 手动净化输出
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
速率限制
javascript
const rateLimit = require('express-rate-limit'); // npm install express-rate-limit
// 基础速率限制
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 限制每个IP在windowMs毫秒内最多访问max次
message: '请求过于频繁,请稍后再试',
standardHeaders: true, // 返回RateLimit-*头
legacyHeaders: false, // 不要`X-RateLimit-*`头
});
app.use(limiter);
// 不同端点的特定限制
const createAccountLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1小时
max: 5, // 每小时最多5次创建账户请求
message: '账户创建请求过多,请稍后再试',
skipSuccessfulRequests: true
});
app.post('/register', createAccountLimiter, (req, res) => {
// 处理注册请求
});
文件上传安全
javascript
const multer = require('multer'); // npm install multer
const path = require('path');
// 安全的文件上传配置
const storage = multer.diskStorage({
destination: function (req, file, cb) {
// 确保上传目录安全
cb(null, 'uploads/');
},
filename: function (req, file, cb) {
// 生成安全的文件名
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
}
});
const fileFilter = (req, file, cb) => {
// 限制文件类型
const allowedTypes = /jpeg|jpg|png|gif|pdf|doc|docx/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (mimetype && extname) {
return cb(null, true);
} else {
cb(new Error('只允许上传图片和文档文件'));
}
};
const upload = multer({
storage: storage,
limits: {
fileSize: 5 * 1024 * 1024 // 限制5MB
},
fileFilter: fileFilter
});
// 安全的文件上传路由
app.post('/upload', upload.single('avatar'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: '未选择文件' });
}
res.json({
message: '文件上传成功',
filename: req.file.filename,
originalName: req.file.originalname,
size: req.file.size
});
});
依赖安全
依赖检查
javascript
// package.json 脚本
{
"scripts": {
"audit": "npm audit",
"audit:fix": "npm audit fix",
"security": "npm audit --audit-level high"
}
}
// 使用Snyk进行安全检查
// 安装: npm install -g snyk
// 检查: snyk test
// 修复: snyk wizard
安全的依赖管理
javascript
// 使用锁文件确保依赖版本固定
// package-lock.json 或 yarn.lock
// 定期更新依赖
const { exec } = require('child_process');
function checkForUpdates() {
exec('npm outdated', (error, stdout, stderr) => {
if (error) {
console.error('检查更新失败:', error);
return;
}
if (stdout) {
console.log('发现过期的包:', stdout);
// 可以自动更新或发送通知
}
});
}
// 定期运行安全审计
function runSecurityAudit() {
exec('npm audit --json', (error, stdout, stderr) => {
if (error) {
console.error('安全审计失败:', error);
return;
}
const auditResult = JSON.parse(stdout);
if (auditResult.vulnerabilities) {
console.log('发现安全漏洞:', auditResult.vulnerabilities);
// 处理安全问题
}
});
}
日志和监控
安全日志记录
javascript
const fs = require('fs');
const path = require('path');
class SecurityLogger {
constructor(logFile = 'security.log') {
this.logFile = path.join(__dirname, logFile);
}
logSecurityEvent(event, details = {}) {
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
event,
details,
ip: details.ip || 'unknown',
userAgent: details.userAgent || 'unknown'
};
const logLine = JSON.stringify(logEntry) + '\n';
fs.appendFileSync(this.logFile, logLine);
}
logFailedLogin(ip, username, userAgent) {
this.logSecurityEvent('FAILED_LOGIN', {
ip,
username,
userAgent,
timestamp: new Date().toISOString()
});
}
logSuspiciousActivity(ip, action, details) {
this.logSecurityEvent('SUSPICIOUS_ACTIVITY', {
ip,
action,
details
});
}
}
const securityLogger = new SecurityLogger();
// 在认证失败时记录日志
app.post('/login', (req, res) => {
const { username, password } = req.body;
// 模拟认证失败
if (true) { // 某种失败条件
securityLogger.logFailedLogin(
req.ip,
username,
req.get('User-Agent')
);
}
});
安全配置示例
Express应用安全配置
javascript
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const cors = require('cors');
const app = express();
// CORS配置
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
credentials: true,
optionsSuccessStatus: 200
}));
// 速率限制
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100,
message: '请求过于频繁',
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', apiLimiter);
// 安全头部
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
frameguard: { action: 'deny' },
referrerPolicy: { policy: 'same-origin' }
}));
// 解析JSON的限制
app.use(express.json({
limit: '10mb',
type: 'application/json'
}));
// 移除X-Powered-By头部
app.disable('x-powered-by');
module.exports = app;
通过实施这些安全最佳实践,可以显著提高Node.js应用的安全性,保护应用和用户数据免受各种安全威胁。