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:
@@ -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
|
||||
|
||||
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;
|
||||
@@ -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"
|
||||
|
||||
12
server.js
12
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();
|
||||
|
||||
Reference in New Issue
Block a user