Skip to content
On this page

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, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

速率限制

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应用的安全性,保护应用和用户数据免受各种安全威胁。