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:
empty
2025-12-27 16:13:55 +08:00
parent 17ddd815a9
commit d1dc095cb1
4 changed files with 167 additions and 0 deletions

View File

@@ -34,3 +34,8 @@ TUNNEL_TOKEN=
# CORS_ENABLED=true # CORS_ENABLED=true
# CORS_ALLOW_ALL=false # CORS_ALLOW_ALL=false
# CORS_ORIGINS=https://app1.com,https://app2.com # 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

146
auth-middleware.js Normal file
View 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;

View File

@@ -100,6 +100,10 @@
"http://127.0.0.1:3000" "http://127.0.0.1:3000"
] ]
}, },
"auth": {
"enabled": false,
"public_models": true
},
"dev_mode": false, "dev_mode": false,
"user_agent": "factory-cli/0.40.2", "user_agent": "factory-cli/0.40.2",
"system_prompt": "You are Droid, an AI software engineering agent built by Factory.\n\n" "system_prompt": "You are Droid, an AI software engineering agent built by Factory.\n\n"

View File

@@ -6,6 +6,7 @@ import { initializeAuth } from './auth.js';
import { initializeUserAgentUpdater } from './user-agent-updater.js'; import { initializeUserAgentUpdater } from './user-agent-updater.js';
import './sls-logger.js'; // 初始化阿里云日志服务 import './sls-logger.js'; // 初始化阿里云日志服务
import { sanitizeForLog } from './log-sanitizer.js'; import { sanitizeForLog } from './log-sanitizer.js';
import { authMiddleware, getAuthConfig } from './auth-middleware.js';
// ============================================================================ // ============================================================================
// 全局错误处理 - 必须在应用启动前注册 // 全局错误处理 - 必须在应用启动前注册
@@ -147,6 +148,9 @@ app.use((req, res, next) => {
next(); next();
}); });
// 请求认证中间件
app.use(authMiddleware);
app.use(router); app.use(router);
app.get('/', (req, res) => { app.get('/', (req, res) => {
@@ -243,6 +247,14 @@ app.use((err, req, res, next) => {
loadConfig(); loadConfig();
logInfo('Configuration loaded successfully'); logInfo('Configuration loaded successfully');
logInfo(`Dev mode: ${isDevMode()}`); 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 // Initialize User-Agent version updater
initializeUserAgentUpdater(); initializeUserAgentUpdater();