llm-chat/backend/middleware/auth.js

272 lines
6.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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