-
-
Notifications
You must be signed in to change notification settings - Fork 180
feat(security): 加固并优化 API Key 与登录鉴权链路(Vacuum Filter → Redis → DB) #734
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
06aed12
66a7cec
31e5cc0
65bfc86
d423e1a
394ba0d
4800447
0c3da04
c4313e9
e6e9511
0700354
18332cb
743b537
f1e6c8d
c48d53d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| const fs = require("node:fs"); | ||
| const path = require("node:path"); | ||
|
|
||
| const src = path.resolve(process.cwd(), "VERSION"); | ||
| const dstDir = path.resolve(process.cwd(), ".next", "standalone"); | ||
| const dst = path.join(dstDir, "VERSION"); | ||
|
|
||
| if (!fs.existsSync(src)) { | ||
| console.error(`[copy-version] VERSION not found at ${src}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| fs.mkdirSync(dstDir, { recursive: true }); | ||
| fs.copyFileSync(src, dst); | ||
| console.log(`[copy-version] Copied VERSION -> ${dst}`); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,18 +3,22 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 在服务器启动时自动执行数据库迁移 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // instrumentation 需要 Node.js runtime(依赖数据库与 Redis 等 Node 能力) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const runtime = "nodejs"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { startCacheCleanup, stopCacheCleanup } from "@/lib/cache/session-cache"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { logger } from "@/lib/logger"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { CHANNEL_API_KEYS_UPDATED, subscribeCacheInvalidation } from "@/lib/redis/pubsub"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { apiKeyVacuumFilter } from "@/lib/security/api-key-vacuum-filter"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // instrumentation 需要 Node.js runtime(依赖数据库与 Redis 等 Node 能力) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const runtime = "nodejs"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const instrumentationState = globalThis as unknown as { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| __CCH_CACHE_CLEANUP_STARTED__?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| __CCH_SHUTDOWN_HOOKS_REGISTERED__?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| __CCH_SHUTDOWN_IN_PROGRESS__?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| __CCH_CLOUD_PRICE_SYNC_STARTED__?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| __CCH_CLOUD_PRICE_SYNC_INTERVAL_ID__?: ReturnType<typeof setInterval>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| __CCH_API_KEY_VF_SYNC_STARTED__?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| __CCH_API_KEY_VF_SYNC_CLEANUP__?: (() => void) | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -82,6 +86,57 @@ async function startCloudPriceSyncScheduler(): Promise<void> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 多实例:订阅 API Key 变更广播,触发本机 Vacuum Filter 失效并重建。 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 目标: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - 避免“本机 filter 漏包含新 key”导致的误拒绝 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - 重建失败/Redis 未配置时自动降级(不阻塞启动) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function startApiKeyVacuumFilterSync(): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (instrumentationState.__CCH_API_KEY_VF_SYNC_STARTED__) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 与 Redis client 的启用条件保持一致:未启用限流/未配置 Redis 时不尝试订阅,避免额外 warn 日志 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rateLimitRaw = process.env.ENABLE_RATE_LIMIT?.trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (rateLimitRaw === "false" || rateLimitRaw === "0" || !process.env.REDIS_URL) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+101
to
+104
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing VF enabled gate
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/instrumentation.ts
Line: 101:104
Comment:
**Missing VF enabled gate**
`startApiKeyVacuumFilterSync()` returns early based on Redis/rate-limit envs, but it doesn’t check whether the Vacuum Filter is actually enabled. As written, setting `ENABLE_RATE_LIMIT`+`REDIS_URL` will subscribe and on every `cch:cache:api_keys:updated` message call `apiKeyVacuumFilter.invalidateAndReload(...)`, which forces background reload attempts even when `ENABLE_API_KEY_VACUUM_FILTER` is off. This can trigger unexpected DB reads and log spam in deployments where VF is intentionally disabled.
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cleanup = await subscribeCacheInvalidation(CHANNEL_API_KEYS_UPDATED, () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| apiKeyVacuumFilter.invalidateAndReload({ reason: "api_keys_updated" }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+96
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. subscribes and reloads VF even when VF is explicitly disabled Currently checks rate limit and Redis config but not whether VF is enabled. If user sets the VF disable flag, subscription still fires and calls Check VF enabled status before subscribing:
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/instrumentation.ts
Line: 96:109
Comment:
subscribes and reloads VF even when VF is explicitly disabled
Currently checks rate limit and Redis config but not whether VF is enabled. If user sets the VF disable flag, subscription still fires and calls `apiKeyVacuumFilter.invalidateAndReload` on every broadcast, triggering unnecessary DB reads and log spam.
Check VF enabled status before subscribing:
```suggestion
async function startApiKeyVacuumFilterSync(): Promise<void> {
if (instrumentationState.__CCH_API_KEY_VF_SYNC_STARTED__) {
return;
}
// Check if VF is enabled (respect explicit disable)
const vfRaw = process.env.ENABLE_API_KEY_VACUUM_FILTER?.trim();
if (vfRaw === "false" || vfRaw === "0") {
return;
}
// 与 Redis client 的启用条件保持一致:未启用限流/未配置 Redis 时不尝试订阅,避免额外 warn 日志
const rateLimitRaw = process.env.ENABLE_RATE_LIMIT?.trim();
if (rateLimitRaw === "false" || rateLimitRaw === "0" || !process.env.REDIS_URL) {
return;
}
```
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!cleanup) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| instrumentationState.__CCH_API_KEY_VF_SYNC_STARTED__ = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| instrumentationState.__CCH_API_KEY_VF_SYNC_CLEANUP__ = cleanup; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info("[Instrumentation] API Key Vacuum Filter sync enabled"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warn("[Instrumentation] API Key Vacuum Filter sync init failed", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: error instanceof Error ? error.message : String(error), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function warmupApiKeyVacuumFilter(): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 预热 API Key Vacuum Filter(减少无效 key 对 DB 的压力) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| apiKeyVacuumFilter.startBackgroundReload({ reason: "startup" }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warn("[Instrumentation] Failed to start API key vacuum filter preload", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: error instanceof Error ? error.message : String(error), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 多实例:订阅 key 变更广播以触发本机 filter 重建 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| void startApiKeyVacuumFilterSync(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function register() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 仅在服务器端执行 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (process.env.NEXT_RUNTIME === "nodejs") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -121,6 +176,15 @@ export async function register() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| instrumentationState.__CCH_API_KEY_VF_SYNC_CLEANUP__?.(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| instrumentationState.__CCH_API_KEY_VF_SYNC_STARTED__ = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warn("[Instrumentation] Failed to cleanup API key vacuum filter sync", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: error instanceof Error ? error.message : String(error), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { stopEndpointProbeScheduler } = await import( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "@/lib/provider-endpoints/probe-scheduler" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -206,6 +270,8 @@ export async function register() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info("[Instrumentation] AUTO_MIGRATE=false: skipping migrations"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| warmupApiKeyVacuumFilter(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 回填 provider_vendors(按域名自动聚合旧 providers) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { backfillProviderVendorsFromProviders } = await import( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -306,6 +372,8 @@ export async function register() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isConnected) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await runMigrations(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| warmupApiKeyVacuumFilter(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 回填 provider_vendors(按域名自动聚合旧 providers) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { backfillProviderVendorsFromProviders } = await import( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Build script may crash
This script uses
require(...)(CommonJS), but this repo may be running Node in ESM mode depending onpackage.json("type": "module"). Iftype=module,node scripts/copy-version-to-standalone.jswill throw at runtime. Please confirm module type; if ESM, convert this file to ESM (import fs from "node:fs") or rename to.cjsand update the build script accordingly.Prompt To Fix With AI