Skip to content
On this page

Express安全性最佳实践

安全性是Web应用开发中最重要的一环。本指南介绍如何在Express应用中实现安全最佳实践,防止常见的安全漏洞。

安全基础设置

使用Helmet保护HTTP头

Helmet通过设置各种HTTP头来帮助保护应用:

javascript
const helmet = require('helmet');

app.use(helmet());

// 或者配置特定的安全头
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      scriptSrc: ["'self'"],
      imgSrc: ["'self'", "data:", "validator.swagger.io"],
      connectSrc: ["'self'", "https://api.example.com"],
    },
  },
  hsts: {
    maxAge: 31536000, // 1年
    includeSubDomains: true,
    preload: true
  },
  frameguard: {
    action: 'deny' // 防止点击劫持
  },
  referrerPolicy: {
    policy: 'same-origin'
  }
}));

输入验证

javascript
const Joi = require('joi');

// 用户注册验证
const registerSchema = Joi.object({
  username: Joi.string().alphanum().min(3).max(30).required(),
  email: Joi.string().email().required(),
  password: Joi.string().min(8).pattern(new RegExp('^[a-zA-Z0-9]{3,30}$')).required(),
  age: Joi.number().integer().min(0).max(120)
});

app.post('/register', async (req, res, next) => {
  try {
    const { error, value } = registerSchema.validate(req.body);
    
    if (error) {
      return res.status(400).json({
        error: 'Validation error',
        details: error.details
      });
    }
    
    // 使用验证后的数据
    req.validatedData = value;
    next();
  } catch (err) {
    next(err);
  }
});

身份验证和授权

JWT认证中间件

javascript
const jwt = require('jsonwebtoken');
const rateLimit = require('express-rate-limit');

// 速率限制
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分钟
  max: 100, // 限制每个IP 15分钟内最多100个请求
  message: 'Too many requests from this IP, please try again later.'
});

app.use('/api/', limiter);

// JWT认证中间件
const authenticateToken = (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

  if (!token) {
    return res.status(401).json({ error: 'Access token required' });
  }

  jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
    if (err) {
      return res.status(403).json({ error: 'Invalid or expired token' });
    }
    req.user = user;
    next();
  });
};

// 使用认证中间件
app.get('/profile', authenticateToken, (req, res) => {
  res.json(req.user);
});

密码安全

javascript
const bcrypt = require('bcrypt');

// 密码哈希
const hashPassword = async (plainPassword) => {
  const saltRounds = 12; // 增加计算复杂度
  const hashedPassword = await bcrypt.hash(plainPassword, saltRounds);
  return hashedPassword;
};

// 密码验证
const validatePassword = async (plainPassword, hashedPassword) => {
  return await bcrypt.compare(plainPassword, hashedPassword);
};

// 用户注册时的安全密码处理
app.post('/register', async (req, res) => {
  try {
    const { username, email, password } = req.body;
    
    // 验证密码强度
    if (!isValidPassword(password)) {
      return res.status(400).json({ error: 'Password does not meet security requirements' });
    }
    
    const hashedPassword = await hashPassword(password);
    
    // 保存用户(示例)
    const user = await User.create({
      username,
      email,
      password: hashedPassword
    });
    
    // 不返回密码
    const { password: _, ...userWithoutPassword } = user.toJSON();
    res.status(201).json(userWithoutPassword);
  } catch (error) {
    res.status(500).json({ error: 'Registration failed' });
  }
});

// 密码强度验证
const isValidPassword = (password) => {
  // 至少8位,包含大小写字母、数字和特殊字符
  const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
  return passwordRegex.test(password);
};

防止常见攻击

防止SQL注入和NoSQL注入

javascript
// 使用参数化查询(示例使用Sequelize)
app.get('/users/:id', async (req, res) => {
  const userId = req.params.id;
  
  // 安全:使用参数化查询
  const user = await User.findByPk(userId);
  
  // 不安全:直接拼接字符串
  // const user = await User.findOne({ where: `id = ${userId}` });
  
  res.json(user);
});

// NoSQL注入防护
app.get('/search', async (req, res) => {
  const { email } = req.query;
  
  // 安全:验证输入
  if (!isValidEmail(email)) {
    return res.status(400).json({ error: 'Invalid email format' });
  }
  
  // 安全:使用安全的查询方式
  const user = await User.findOne({ email });
  
  res.json(user);
});

const isValidEmail = (email) => {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
};

防止跨站脚本攻击(XSS)

javascript
const xss = require('xss');

// 输入清理中间件
const sanitizeInput = (req, res, next) => {
  if (req.body) {
    req.body = sanitizeObject(req.body);
  }
  if (req.query) {
    req.query = sanitizeObject(req.query);
  }
  if (req.params) {
    req.params = sanitizeObject(req.params);
  }
  next();
};

const sanitizeObject = (obj) => {
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }
  
  for (const key in obj) {
    if (typeof obj[key] === 'string') {
      obj[key] = xss(obj[key]);
    } else if (typeof obj[key] === 'object') {
      obj[key] = sanitizeObject(obj[key]);
    }
  }
  
  return obj;
};

app.use(sanitizeInput);

// 输出转义
app.get('/profile/:id', async (req, res) => {
  const user = await User.findById(req.params.id);
  
  // 模板引擎会自动转义,但手动转义更安全
  res.render('profile', {
    user: {
      ...user,
      bio: xss(user.bio) // 确保bio字段安全
    }
  });
});

防止跨站请求伪造(CSRF)

javascript
const csrf = require('csurf');
const cookieParser = require('cookie-parser');

app.use(cookieParser());
app.use(csrf({ cookie: true }));

// CSRF保护中间件
app.use((req, res, next) => {
  res.locals.csrfToken = req.csrfToken();
  next();
});

// 需要CSRF保护的路由
app.post('/transfer', (req, res) => {
  // 验证通过,执行转账操作
  res.json({ message: 'Transfer successful' });
});

数据验证和清理

请求大小限制

javascript
// 限制请求体大小
app.use(express.json({ 
  limit: '10mb',
  type: 'application/json'
}));

app.use(express.urlencoded({ 
  limit: '10mb',
  extended: true
}));

// 限制文件上传大小
app.use(multer({
  limits: {
    fileSize: 5 * 1024 * 1024, // 5MB
  }
}).single('file'));

参数验证中间件

javascript
// 通用参数验证中间件
const validateParams = (schema) => {
  return (req, res, next) => {
    const { error } = schema.validate({
      ...req.params,
      ...req.body,
      ...req.query
    });
    
    if (error) {
      return res.status(400).json({
        error: 'Validation failed',
        details: error.details
      });
    }
    
    next();
  };
};

// 使用验证中间件
const userIdSchema = Joi.object({
  id: Joi.string().length(24).hex().required() // MongoDB ObjectId格式
});

app.get('/users/:id', validateParams(userIdSchema), async (req, res) => {
  const user = await User.findById(req.params.id);
  res.json(user);
});

会话安全

安全会话配置

javascript
const session = require('express-session');
const MongoStore = require('connect-mongo');

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  name: 'sessionId', // 更改默认session名称
  cookie: {
    secure: process.env.NODE_ENV === 'production', // 仅在HTTPS下发送
    httpOnly: true, // 防止XSS访问cookie
    maxAge: 24 * 60 * 60 * 1000, // 24小时
    sameSite: 'strict' // 防止CSRF
  },
  store: MongoStore.create({
    mongoUrl: process.env.MONGODB_URI
  })
}));

API安全

API密钥验证

javascript
const apiKeys = new Set([process.env.API_KEY]);

const validateApiKey = (req, res, next) => {
  const apiKey = req.headers['x-api-key'];
  
  if (!apiKey || !apiKeys.has(apiKey)) {
    return res.status(401).json({ error: 'Invalid API key' });
  }
  
  next();
};

app.use('/api/private', validateApiKey);

请求频率限制

javascript
const rateLimit = require('express-rate-limit');
const slowDown = require('express-slow-down');

// 基本速率限制
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分钟
  max: 100, // 限制每个IP 100个请求
  message: 'Too many requests, please try again later',
  standardHeaders: true, // 返回RateLimit头
  legacyHeaders: false, // 不使用X-RateLimit头
});

app.use('/api/', apiLimiter);

// 逐渐减慢请求速度
const speedLimiter = slowDown({
  windowMs: 15 * 60 * 1000, // 15分钟
  delayAfter: 50, // 前50个请求正常响应
  delayMs: 500 // 之后每个请求延迟500ms
});

app.use('/api/', speedLimiter);

环境安全

环境变量管理

javascript
// 使用dotenv管理环境变量
require('dotenv').config();

// 验证必需的环境变量
const requiredEnvVars = [
  'NODE_ENV',
  'PORT',
  'DB_URI',
  'JWT_SECRET',
  'SESSION_SECRET'
];

const missingEnvVars = requiredEnvVars.filter(envVar => !process.env[envVar]);
if (missingEnvVars.length > 0) {
  console.error('Missing required environment variables:', missingEnvVars);
  process.exit(1);
}

错误处理安全

javascript
// 安全的错误处理
app.use((err, req, res, next) => {
  // 记录错误(仅在开发环境显示完整错误)
  console.error(err);

  // 在生产环境中不暴露敏感错误信息
  if (process.env.NODE_ENV === 'production') {
    res.status(500).json({
      error: 'Internal server error'
    });
  } else {
    res.status(500).json({
      error: err.message,
      stack: err.stack
    });
  }
});

安全监控

安全事件日志

javascript
const winston = require('winston');

const securityLogger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'security.log' })
  ]
});

// 记录安全相关事件
app.use('/admin', (req, res, next) => {
  securityLogger.info('Admin access attempt', {
    ip: req.ip,
    userAgent: req.get('User-Agent'),
    url: req.originalUrl,
    timestamp: new Date().toISOString()
  });
  next();
});

安全检查清单

部署前安全检查

  1. 禁用错误详细信息 - 生产环境中不显示错误堆栈
  2. 设置安全头 - 使用Helmet设置安全HTTP头
  3. 验证所有输入 - 验证所有用户输入
  4. 使用HTTPS - 强制使用HTTPS
  5. 限制请求大小 - 防止大型请求攻击
  6. 实施速率限制 - 防止暴力攻击
  7. 使用安全的会话配置 - 设置适当的cookie选项
  8. 清理敏感数据 - 不在日志中记录敏感信息
  9. 使用强密码策略 - 实施密码复杂度要求
  10. 定期更新依赖 - 保持依赖包更新

通过实施这些安全最佳实践,可以显著提高Express应用的安全性,保护用户数据和系统资源。