272 lines
6.1 KiB
JavaScript
272 lines
6.1 KiB
JavaScript
const { Logger } = require('./logger');
|
||
const { ForbiddenError, ValidationError } = require('./errorHandler');
|
||
|
||
/**
|
||
* 简单的API密钥认证中间件
|
||
*/
|
||
const apiKeyAuth = (req, res, next) => {
|
||
const apiKey = req.headers['x-api-key'] || req.query.apiKey;
|
||
const validApiKey = process.env.API_KEY;
|
||
|
||
// 如果没有配置API密钥,跳过验证
|
||
if (!validApiKey) {
|
||
return next();
|
||
}
|
||
|
||
if (!apiKey) {
|
||
Logger.warn('缺少API密钥', {
|
||
ip: req.ip,
|
||
path: req.path,
|
||
userAgent: req.get('User-Agent')
|
||
});
|
||
|
||
return res.status(401).json({
|
||
success: false,
|
||
error: '缺少API密钥',
|
||
code: 'MISSING_API_KEY'
|
||
});
|
||
}
|
||
|
||
if (apiKey !== validApiKey) {
|
||
Logger.warn('无效的API密钥', {
|
||
ip: req.ip,
|
||
path: req.path,
|
||
providedKey: apiKey.substring(0, 8) + '...',
|
||
userAgent: req.get('User-Agent')
|
||
});
|
||
|
||
return res.status(401).json({
|
||
success: false,
|
||
error: '无效的API密钥',
|
||
code: 'INVALID_API_KEY'
|
||
});
|
||
}
|
||
|
||
next();
|
||
};
|
||
|
||
/**
|
||
* 会话验证中间件
|
||
*/
|
||
const sessionAuth = (req, res, next) => {
|
||
const sessionId = req.headers['x-session-id'];
|
||
|
||
// 如果没有会话ID,生成一个临时的
|
||
if (!sessionId) {
|
||
req.sessionId = `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||
Logger.debug('生成临时会话ID', { sessionId: req.sessionId });
|
||
} else {
|
||
req.sessionId = sessionId;
|
||
Logger.debug('使用现有会话ID', { sessionId });
|
||
}
|
||
|
||
// 设置响应头
|
||
res.setHeader('x-session-id', req.sessionId);
|
||
|
||
next();
|
||
};
|
||
|
||
/**
|
||
* 基础权限检查中间件
|
||
*/
|
||
const basicPermission = (requiredPermission) => {
|
||
return (req, res, next) => {
|
||
// 这里可以实现更复杂的权限逻辑
|
||
// 目前只是一个示例框架
|
||
|
||
const userPermissions = req.headers['x-user-permissions']?.split(',') || [];
|
||
|
||
if (requiredPermission && !userPermissions.includes(requiredPermission)) {
|
||
Logger.warn('权限不足', {
|
||
ip: req.ip,
|
||
path: req.path,
|
||
requiredPermission,
|
||
userPermissions
|
||
});
|
||
|
||
throw new ForbiddenError('权限不足');
|
||
}
|
||
|
||
next();
|
||
};
|
||
};
|
||
|
||
/**
|
||
* 管理员权限检查
|
||
*/
|
||
const adminOnly = (req, res, next) => {
|
||
const isAdmin = req.headers['x-user-role'] === 'admin';
|
||
|
||
if (!isAdmin) {
|
||
Logger.warn('非管理员尝试访问管理接口', {
|
||
ip: req.ip,
|
||
path: req.path,
|
||
userRole: req.headers['x-user-role']
|
||
});
|
||
|
||
throw new ForbiddenError('需要管理员权限');
|
||
}
|
||
|
||
next();
|
||
};
|
||
|
||
/**
|
||
* 请求来源验证中间件
|
||
*/
|
||
const originCheck = (req, res, next) => {
|
||
const origin = req.headers.origin || req.headers.referer;
|
||
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
|
||
|
||
// 开发环境跳过来源检查
|
||
if (process.env.NODE_ENV === 'development') {
|
||
return next();
|
||
}
|
||
|
||
// 如果没有配置允许的来源,跳过检查
|
||
if (allowedOrigins.length === 0) {
|
||
return next();
|
||
}
|
||
|
||
if (!origin || !allowedOrigins.some(allowed => origin.includes(allowed))) {
|
||
Logger.warn('不允许的请求来源', {
|
||
origin,
|
||
allowedOrigins,
|
||
ip: req.ip,
|
||
path: req.path
|
||
});
|
||
|
||
return res.status(403).json({
|
||
success: false,
|
||
error: '不允许的请求来源',
|
||
code: 'FORBIDDEN_ORIGIN'
|
||
});
|
||
}
|
||
|
||
next();
|
||
};
|
||
|
||
/**
|
||
* 用户代理检查中间件
|
||
*/
|
||
const userAgentCheck = (req, res, next) => {
|
||
const userAgent = req.get('User-Agent');
|
||
const blockedAgents = process.env.BLOCKED_USER_AGENTS?.split(',') || [];
|
||
|
||
if (!userAgent) {
|
||
Logger.warn('缺少User-Agent头部', {
|
||
ip: req.ip,
|
||
path: req.path
|
||
});
|
||
|
||
// 可以选择是否阻止没有User-Agent的请求
|
||
if (process.env.REQUIRE_USER_AGENT === 'true') {
|
||
return res.status(400).json({
|
||
success: false,
|
||
error: '缺少User-Agent头部',
|
||
code: 'MISSING_USER_AGENT'
|
||
});
|
||
}
|
||
}
|
||
|
||
// 检查是否在黑名单中
|
||
if (userAgent && blockedAgents.some(blocked => userAgent.includes(blocked))) {
|
||
Logger.warn('被阻止的User-Agent', {
|
||
userAgent,
|
||
ip: req.ip,
|
||
path: req.path
|
||
});
|
||
|
||
return res.status(403).json({
|
||
success: false,
|
||
error: '不允许的客户端',
|
||
code: 'BLOCKED_USER_AGENT'
|
||
});
|
||
}
|
||
|
||
next();
|
||
};
|
||
|
||
/**
|
||
* 请求大小限制中间件
|
||
*/
|
||
const requestSizeLimit = (maxSize = '10mb') => {
|
||
return (req, res, next) => {
|
||
const contentLength = parseInt(req.headers['content-length'] || '0');
|
||
const maxSizeBytes = parseSize(maxSize);
|
||
|
||
if (contentLength > maxSizeBytes) {
|
||
Logger.warn('请求体过大', {
|
||
contentLength,
|
||
maxSize: maxSizeBytes,
|
||
ip: req.ip,
|
||
path: req.path
|
||
});
|
||
|
||
return res.status(413).json({
|
||
success: false,
|
||
error: '请求体过大',
|
||
code: 'PAYLOAD_TOO_LARGE',
|
||
maxSize: maxSize
|
||
});
|
||
}
|
||
|
||
next();
|
||
};
|
||
};
|
||
|
||
/**
|
||
* 解析大小字符串(如 '10mb', '1gb')
|
||
*/
|
||
function parseSize(sizeStr) {
|
||
const units = {
|
||
'b': 1,
|
||
'kb': 1024,
|
||
'mb': 1024 * 1024,
|
||
'gb': 1024 * 1024 * 1024
|
||
};
|
||
|
||
const match = sizeStr.toLowerCase().match(/^(\d+(?:\.\d+)?)(b|kb|mb|gb)$/);
|
||
if (!match) {
|
||
throw new Error(`无效的大小格式: ${sizeStr}`);
|
||
}
|
||
|
||
const [, size, unit] = match;
|
||
return parseFloat(size) * units[unit];
|
||
}
|
||
|
||
/**
|
||
* 内容类型验证中间件
|
||
*/
|
||
const contentTypeCheck = (allowedTypes = ['application/json']) => {
|
||
return (req, res, next) => {
|
||
// 只对有请求体的方法进行检查
|
||
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
|
||
const contentType = req.headers['content-type'];
|
||
|
||
if (!contentType) {
|
||
throw new ValidationError('缺少Content-Type头部');
|
||
}
|
||
|
||
const isAllowed = allowedTypes.some(type =>
|
||
contentType.toLowerCase().includes(type.toLowerCase())
|
||
);
|
||
|
||
if (!isAllowed) {
|
||
throw new ValidationError(`不支持的Content-Type: ${contentType}`);
|
||
}
|
||
}
|
||
|
||
next();
|
||
};
|
||
};
|
||
|
||
module.exports = {
|
||
apiKeyAuth,
|
||
sessionAuth,
|
||
basicPermission,
|
||
adminOnly,
|
||
originCheck,
|
||
userAgentCheck,
|
||
requestSizeLimit,
|
||
contentTypeCheck
|
||
}; |