Appearance
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();
});
安全检查清单
部署前安全检查
- 禁用错误详细信息 - 生产环境中不显示错误堆栈
- 设置安全头 - 使用Helmet设置安全HTTP头
- 验证所有输入 - 验证所有用户输入
- 使用HTTPS - 强制使用HTTPS
- 限制请求大小 - 防止大型请求攻击
- 实施速率限制 - 防止暴力攻击
- 使用安全的会话配置 - 设置适当的cookie选项
- 清理敏感数据 - 不在日志中记录敏感信息
- 使用强密码策略 - 实施密码复杂度要求
- 定期更新依赖 - 保持依赖包更新
通过实施这些安全最佳实践,可以显著提高Express应用的安全性,保护用户数据和系统资源。