From d1dc095cb175db5e223f0d45f4db819b718e1654 Mon Sep 17 00:00:00 2001 From: empty Date: Sat, 27 Dec 2025 16:13:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E4=B8=AD=E9=97=B4=E4=BB=B6=E4=BF=9D=E6=8A=A4?= =?UTF-8?q?=20API=20=E7=AB=AF=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 auth-middleware.js 验证客户端 API Key - 支持 Authorization: Bearer 和 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 --- .env.example | 5 ++ auth-middleware.js | 146 +++++++++++++++++++++++++++++++++++++++++++++ config.json | 4 ++ server.js | 12 ++++ 4 files changed, 167 insertions(+) create mode 100644 auth-middleware.js diff --git a/.env.example b/.env.example index a7fcb54..1e5bf36 100644 --- a/.env.example +++ b/.env.example @@ -34,3 +34,8 @@ TUNNEL_TOKEN= # CORS_ENABLED=true # CORS_ALLOW_ALL=false # CORS_ORIGINS=https://app1.com,https://app2.com + +# API Authentication - Protect your API endpoints +# AUTH_ENABLED=true # Enable authentication (required for production) +# API_KEYS=sk-key1,sk-key2,sk-key3 # Comma-separated API keys (ONLY via env var for security) +# AUTH_PUBLIC_MODELS=true # Allow /v1/models without auth diff --git a/auth-middleware.js b/auth-middleware.js new file mode 100644 index 0000000..3fdad8a --- /dev/null +++ b/auth-middleware.js @@ -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 或 x-api-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; diff --git a/config.json b/config.json index e6f635e..57f96e9 100644 --- a/config.json +++ b/config.json @@ -100,6 +100,10 @@ "http://127.0.0.1:3000" ] }, + "auth": { + "enabled": false, + "public_models": true + }, "dev_mode": false, "user_agent": "factory-cli/0.40.2", "system_prompt": "You are Droid, an AI software engineering agent built by Factory.\n\n" diff --git a/server.js b/server.js index b974e2d..a516227 100644 --- a/server.js +++ b/server.js @@ -6,6 +6,7 @@ import { initializeAuth } from './auth.js'; import { initializeUserAgentUpdater } from './user-agent-updater.js'; import './sls-logger.js'; // 初始化阿里云日志服务 import { sanitizeForLog } from './log-sanitizer.js'; +import { authMiddleware, getAuthConfig } from './auth-middleware.js'; // ============================================================================ // 全局错误处理 - 必须在应用启动前注册 @@ -147,6 +148,9 @@ app.use((req, res, next) => { next(); }); +// 请求认证中间件 +app.use(authMiddleware); + app.use(router); app.get('/', (req, res) => { @@ -243,6 +247,14 @@ app.use((err, req, res, next) => { loadConfig(); logInfo('Configuration loaded successfully'); logInfo(`Dev mode: ${isDevMode()}`); + + // Log auth status + const authConfig = getAuthConfig(); + if (authConfig.enabled) { + logInfo(`Auth enabled with ${authConfig.apiKeys.size} API key(s)`); + } else { + logInfo('Auth disabled - API endpoints are publicly accessible'); + } // Initialize User-Agent version updater initializeUserAgentUpdater();