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_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
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"
|
"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"
|
||||||
|
|||||||
12
server.js
12
server.js
@@ -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) => {
|
||||||
@@ -244,6 +248,14 @@ app.use((err, req, res, next) => {
|
|||||||
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();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user