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
|
|||
|
};
|