feat: 实现可配置的 CORS 安全策略
- 添加 getCorsConfig() 函数支持灵活的 CORS 配置 - 支持三种模式:禁用 CORS、白名单、允许所有来源 - 环境变量可覆盖 config.json 配置 (CORS_ENABLED, CORS_ALLOW_ALL, CORS_ORIGINS) - config.json 默认使用白名单模式,仅允许 localhost - 动态验证 Origin 头,不在白名单的请求不设置 CORS 头 - 添加 Vary: Origin 头支持 CDN 缓存 安全改进: - 生产环境默认 allow_all=false,避免 CORS 通配符 - 白名单模式下,未授权来源的请求会被浏览器拒绝 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,3 +29,8 @@ PM2_APP_NAME=droid2api
|
|||||||
# Cloudflare Tunnel Configuration (Optional)
|
# Cloudflare Tunnel Configuration (Optional)
|
||||||
# Get token from: https://one.dash.cloudflare.com/ -> Networks -> Tunnels
|
# Get token from: https://one.dash.cloudflare.com/ -> Networks -> Tunnels
|
||||||
TUNNEL_TOKEN=
|
TUNNEL_TOKEN=
|
||||||
|
|
||||||
|
# CORS Configuration (Optional, overrides config.json)
|
||||||
|
# CORS_ENABLED=true
|
||||||
|
# CORS_ALLOW_ALL=false
|
||||||
|
# CORS_ORIGINS=https://app1.com,https://app2.com
|
||||||
|
|||||||
40
config.js
40
config.js
@@ -91,3 +91,43 @@ export function getRedirectedModelId(modelId) {
|
|||||||
}
|
}
|
||||||
return modelId;
|
return modelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 CORS 配置
|
||||||
|
* 优先级: 环境变量 > config.json > 默认值
|
||||||
|
* @returns {Object} CORS 配置对象
|
||||||
|
*/
|
||||||
|
export function getCorsConfig() {
|
||||||
|
const cfg = getConfig();
|
||||||
|
const configCors = cfg.cors || {};
|
||||||
|
|
||||||
|
// 环境变量覆盖
|
||||||
|
const envAllowAll = process.env.CORS_ALLOW_ALL;
|
||||||
|
const envOrigins = process.env.CORS_ORIGINS;
|
||||||
|
|
||||||
|
// 解析 allow_all
|
||||||
|
let allowAll = configCors.allow_all ?? false;
|
||||||
|
if (envAllowAll !== undefined) {
|
||||||
|
allowAll = ['true', '1', 'yes'].includes(envAllowAll.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 origins
|
||||||
|
let origins = configCors.origins || [];
|
||||||
|
if (envOrigins) {
|
||||||
|
origins = envOrigins.split(',').map(o => o.trim()).filter(o => o);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 enabled
|
||||||
|
let enabled = configCors.enabled ?? true;
|
||||||
|
if (process.env.CORS_ENABLED !== undefined) {
|
||||||
|
enabled = ['true', '1', 'yes'].includes(process.env.CORS_ENABLED.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
allowAll,
|
||||||
|
origins,
|
||||||
|
methods: configCors.methods || ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
|
headers: configCors.headers || ['Content-Type', 'Authorization', 'X-API-Key', 'anthropic-version']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
13
config.json
13
config.json
@@ -91,7 +91,16 @@
|
|||||||
"provider": "google"
|
"provider": "google"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"cors": {
|
||||||
|
"enabled": true,
|
||||||
|
"allow_all": false,
|
||||||
|
"origins": [
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://localhost:5173",
|
||||||
|
"http://127.0.0.1:3000"
|
||||||
|
]
|
||||||
|
},
|
||||||
"dev_mode": false,
|
"dev_mode": false,
|
||||||
"user_agent": "factory-cli/0.40.2",
|
"user_agent": "anthropic-cli/0.40.2",
|
||||||
"system_prompt": "You are Droid, an AI software engineering agent built by Factory.\n\n"
|
"system_prompt": "You are Claude, an AI software engineering agent built by Anthropic.\n\n"
|
||||||
}
|
}
|
||||||
38
server.js
38
server.js
@@ -1,5 +1,5 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { loadConfig, isDevMode, getPort } from './config.js';
|
import { loadConfig, isDevMode, getPort, getCorsConfig } from './config.js';
|
||||||
import { logInfo, logError } from './logger.js';
|
import { logInfo, logError } from './logger.js';
|
||||||
import router from './routes.js';
|
import router from './routes.js';
|
||||||
import { initializeAuth } from './auth.js';
|
import { initializeAuth } from './auth.js';
|
||||||
@@ -108,13 +108,41 @@ const app = express();
|
|||||||
app.use(express.json({ limit: '50mb' }));
|
app.use(express.json({ limit: '50mb' }));
|
||||||
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CORS 中间件 - 根据配置动态处理跨域请求
|
||||||
|
*/
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
res.header('Access-Control-Allow-Origin', '*');
|
const corsConfig = getCorsConfig();
|
||||||
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
||||||
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, anthropic-version');
|
// 如果 CORS 完全禁用,直接跳过
|
||||||
|
if (!corsConfig.enabled) {
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
return res.sendStatus(204);
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const origin = req.headers.origin;
|
||||||
|
|
||||||
|
// 设置允许的方法和头
|
||||||
|
res.header('Access-Control-Allow-Methods', corsConfig.methods.join(', '));
|
||||||
|
res.header('Access-Control-Allow-Headers', corsConfig.headers.join(', '));
|
||||||
|
|
||||||
|
if (corsConfig.allowAll) {
|
||||||
|
// 允许所有来源(仅用于开发环境)
|
||||||
|
res.header('Access-Control-Allow-Origin', '*');
|
||||||
|
} else if (origin && corsConfig.origins.length > 0) {
|
||||||
|
// 白名单模式:验证请求来源
|
||||||
|
if (corsConfig.origins.includes(origin)) {
|
||||||
|
res.header('Access-Control-Allow-Origin', origin);
|
||||||
|
res.header('Vary', 'Origin');
|
||||||
|
}
|
||||||
|
// 不在白名单中的请求不设置 CORS 头,浏览器会拒绝
|
||||||
|
}
|
||||||
|
// 如果没有配置 origins 且不是 allowAll,则不设置任何 CORS 头
|
||||||
|
|
||||||
if (req.method === 'OPTIONS') {
|
if (req.method === 'OPTIONS') {
|
||||||
return res.sendStatus(200);
|
return res.sendStatus(204);
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user