feat: 添加请求认证中间件保护 API 端点
- 新增 auth-middleware.js 验证客户端 API Key - 支持 Authorization: Bearer <key> 和 x-api-key 两种方式 - API Keys 只通过环境变量配置(安全最佳实践) - 公开路径: /, /health, /status - 可配置 /v1/models 是否需要认证 - 启动时输出认证状态日志 配置方式: AUTH_ENABLED=true API_KEYS=sk-key1,sk-key2 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
146
auth-middleware.js
Normal file
146
auth-middleware.js
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 请求认证中间件
|
||||
* 验证客户端请求的 API Key,保护 API 端点
|
||||
*/
|
||||
|
||||
import { getConfig } from './config.js';
|
||||
import { logInfo, logError } from './logger.js';
|
||||
|
||||
// 不需要认证的路径(精确匹配)
|
||||
const PUBLIC_PATHS = new Set([
|
||||
'/',
|
||||
'/health',
|
||||
'/status'
|
||||
]);
|
||||
|
||||
// 可配置是否公开的路径
|
||||
const OPTIONAL_PUBLIC_PATHS = new Set([
|
||||
'/v1/models'
|
||||
]);
|
||||
|
||||
/**
|
||||
* 获取认证配置
|
||||
* API Keys 只从环境变量读取(安全考虑)
|
||||
* enabled/public_models 可从 config.json 读取默认值,环境变量可覆盖
|
||||
*/
|
||||
export function getAuthConfig() {
|
||||
const cfg = getConfig();
|
||||
const configAuth = cfg.auth || {};
|
||||
|
||||
// 环境变量
|
||||
const envEnabled = process.env.AUTH_ENABLED;
|
||||
const envApiKeys = process.env.API_KEYS;
|
||||
const envPublicModels = process.env.AUTH_PUBLIC_MODELS;
|
||||
|
||||
// 解析 enabled(环境变量 > config.json)
|
||||
let enabled = configAuth.enabled ?? false;
|
||||
if (envEnabled !== undefined) {
|
||||
enabled = ['true', '1', 'yes'].includes(envEnabled.toLowerCase());
|
||||
}
|
||||
|
||||
// API Keys 只从环境变量读取(敏感信息不应存储在配置文件中)
|
||||
let apiKeys = [];
|
||||
if (envApiKeys) {
|
||||
apiKeys = envApiKeys.split(',').map(k => k.trim()).filter(k => k);
|
||||
}
|
||||
|
||||
// 解析 public_models(环境变量 > config.json)
|
||||
let publicModels = configAuth.public_models ?? true;
|
||||
if (envPublicModels !== undefined) {
|
||||
publicModels = ['true', '1', 'yes'].includes(envPublicModels.toLowerCase());
|
||||
}
|
||||
|
||||
return {
|
||||
enabled,
|
||||
apiKeys: new Set(apiKeys),
|
||||
publicModels
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头中提取 API Key
|
||||
* 支持: Authorization: Bearer <key> 或 x-api-key: <key>
|
||||
*/
|
||||
function extractApiKey(req) {
|
||||
// 优先检查 Authorization header
|
||||
const authHeader = req.headers.authorization;
|
||||
if (authHeader) {
|
||||
if (authHeader.startsWith('Bearer ')) {
|
||||
return authHeader.slice(7).trim();
|
||||
}
|
||||
// 也支持直接传 key(不带 Bearer 前缀)
|
||||
return authHeader.trim();
|
||||
}
|
||||
|
||||
// 其次检查 x-api-key header
|
||||
const xApiKey = req.headers['x-api-key'];
|
||||
if (xApiKey) {
|
||||
return xApiKey.trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证中间件
|
||||
*/
|
||||
export function authMiddleware(req, res, next) {
|
||||
const authConfig = getAuthConfig();
|
||||
|
||||
// 如果认证未启用,直接放行
|
||||
if (!authConfig.enabled) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// 检查是否是公开路径
|
||||
if (PUBLIC_PATHS.has(req.path)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// 检查可选公开路径
|
||||
if (authConfig.publicModels && OPTIONAL_PUBLIC_PATHS.has(req.path)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// 检查 API Keys 是否配置
|
||||
if (authConfig.apiKeys.size === 0) {
|
||||
logError('Auth enabled but no API keys configured');
|
||||
return res.status(500).json({
|
||||
error: {
|
||||
message: 'Server configuration error: authentication enabled but no API keys configured',
|
||||
type: 'server_error',
|
||||
code: 'auth_not_configured'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 提取并验证 API Key
|
||||
const clientKey = extractApiKey(req);
|
||||
|
||||
if (!clientKey) {
|
||||
logInfo(`Auth failed: No API key provided for ${req.method} ${req.path}`);
|
||||
return res.status(401).json({
|
||||
error: {
|
||||
message: 'Missing API key. Please include your API key in the Authorization header using Bearer auth (Authorization: Bearer YOUR_API_KEY) or as x-api-key header.',
|
||||
type: 'authentication_error',
|
||||
code: 'missing_api_key'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!authConfig.apiKeys.has(clientKey)) {
|
||||
logInfo(`Auth failed: Invalid API key for ${req.method} ${req.path}`);
|
||||
return res.status(401).json({
|
||||
error: {
|
||||
message: 'Invalid API key provided.',
|
||||
type: 'authentication_error',
|
||||
code: 'invalid_api_key'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 认证通过
|
||||
next();
|
||||
}
|
||||
|
||||
export default authMiddleware;
|
||||
Reference in New Issue
Block a user