llm-chat/backend/middleware/auth.js

272 lines
6.1 KiB
JavaScript
Raw Normal View History

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