From 90ff15b62707139342c7c01d760ca63c7d6f96ca Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 22 Aug 2026 19:10:06 +0800 Subject: [PATCH] =?UTF-8?q?=E7=9F=AD=E4=BF=A1=E5=8F=91=E9=80=81=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E5=A2=9E=E5=8A=A0=E5=86=B7=E5=8D=B4=E9=98=B2=E8=BF=9E?= =?UTF-8?q?=E7=82=B9=E9=99=90=E6=B5=81=E5=B9=B6=E9=85=8D=E7=BD=AEtrust=20p?= =?UTF-8?q?roxy=E6=81=A2=E5=A4=8D=E7=9C=9F=E5=AE=9E=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AFIP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.mac-docker.example | 2 ++ .env.server.example | 2 ++ apps/backend/src/app.ts | 5 ++++ apps/backend/src/config/defaults.ts | 2 ++ apps/backend/src/config/env-overrides.ts | 26 +++++++++++++++++++ apps/backend/src/config/runtime-validation.ts | 13 ++++++++++ apps/backend/src/types/runtime-config.ts | 2 ++ .../src/pages/worker/WorkerLoginPage.tsx | 5 ++++ docker-compose.dev.yml | 1 + docker-compose.yml | 1 + 10 files changed, 59 insertions(+) diff --git a/.env.mac-docker.example b/.env.mac-docker.example index 4ef44611..87b1a1d4 100644 --- a/.env.mac-docker.example +++ b/.env.mac-docker.example @@ -7,6 +7,8 @@ CADDY_SITE_ADDR=http://localhost WORKER_SITE_ADDR=http://worker.localhost TZ=Asia/Shanghai BACKEND_PORT=3000 +# 反向代理层数:Caddy 后保持 1(默认),后端直连暴露时设为 false +HTTP_TRUST_PROXY=1 # Claim Links CLAIM_BASE_URL=http://localhost/#/claim diff --git a/.env.server.example b/.env.server.example index be140725..408d75ae 100644 --- a/.env.server.example +++ b/.env.server.example @@ -7,6 +7,8 @@ CADDY_SITE_ADDR=https://order.khhao.com WORKER_SITE_ADDR=https://worker.khhao.com TZ=Asia/Shanghai BACKEND_PORT=3000 +# 反向代理层数:Caddy 后保持 1(默认),后端直连暴露时设为 false +HTTP_TRUST_PROXY=1 # 构建镜像源(可选,默认阿里云公共源) # 阿里云 ECS 同地域内网可改为 mirrors.cloud.aliyuncs.com 加速 apt/apk diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index d53389ac..edb78006 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -27,6 +27,11 @@ type CreateAppOptions = { export function createApp({ startupState, isShutdownStarted, config }: CreateAppOptions) { const app = express() + // 部署在反代(Caddy)之后时让 req.ip 取 X-Forwarded-For 的真实客户端地址;限流、访问日志与登录设备记录都依赖它。 + if (config.server?.trustProxy !== undefined && config.server?.trustProxy !== null) { + app.set('trust proxy', config.server.trustProxy) + } + app.use(accessLogMiddleware) app.use(createCorsMiddleware(config)) app.use( diff --git a/apps/backend/src/config/defaults.ts b/apps/backend/src/config/defaults.ts index 5b380b1e..31277c59 100644 --- a/apps/backend/src/config/defaults.ts +++ b/apps/backend/src/config/defaults.ts @@ -6,6 +6,8 @@ export function createDefaultRuntimeConfig(projectRoot: string): RuntimeConfig { return { server: { port: 3000, + // 默认信任一层反代(Caddy),req.ip 才能取到真实客户端地址;直连暴露端口时设为 false。 + trustProxy: 1, }, data: { diff --git a/apps/backend/src/config/env-overrides.ts b/apps/backend/src/config/env-overrides.ts index 6bd94d32..b72ffc98 100644 --- a/apps/backend/src/config/env-overrides.ts +++ b/apps/backend/src/config/env-overrides.ts @@ -28,6 +28,7 @@ type EnvOverride = { export const ENV_OVERRIDES: readonly EnvOverride[] = [ integerEnv('PORT', ['server', 'port']), + trustProxyEnv('HTTP_TRUST_PROXY', ['server', 'trustProxy']), stringEnv('DATA_ROOT', ['data', 'root'], path.resolve), stringEnv('LOG_LEVEL', ['logging', 'level']), stringEnv('LOG_INTEGRATION_LEVEL', ['logging', 'integrationLevel']), @@ -258,6 +259,31 @@ function booleanEnv(env: string, configPath: RuntimeConfigPath): EnvOverride { } } +function trustProxyEnv(env: string, configPath: RuntimeConfigPath): EnvOverride { + return { + env, + path: configPath, + read: parseTrustProxy, + } +} + +/** HTTP_TRUST_PROXY 支持 true/false、反代层数(数字)与 loopback 等预定义子网,其余值回落到默认。 */ +function parseTrustProxy(rawValue: unknown): RuntimeConfigValue | null { + const value = parseString(rawValue) + if (value === null) return null + const normalized = value.trim().toLowerCase() + if (normalized === 'true' || normalized === 'yes') return true + if (normalized === 'false' || normalized === 'no') return false + if (normalized === 'loopback' || normalized === 'uniquelocal' || normalized === 'linklocal') { + return normalized + } + if (/^\d+$/.test(normalized)) { + const hops = Number(normalized) + return hops >= 0 ? hops : null + } + return null +} + function adminUsersJsonEnv(env: string, configPath: RuntimeConfigPath): EnvOverride { return { env, diff --git a/apps/backend/src/config/runtime-validation.ts b/apps/backend/src/config/runtime-validation.ts index c1fa79bf..0f956827 100644 --- a/apps/backend/src/config/runtime-validation.ts +++ b/apps/backend/src/config/runtime-validation.ts @@ -44,6 +44,13 @@ export function validateRuntimeConfig( const productionLike = isProductionLike(env) requireInteger(issues, 'server.port', config.server?.port, { min: 1, max: 65535 }) + const trustProxy = config.server?.trustProxy + if (trustProxy !== undefined && trustProxy !== null && !isValidTrustProxy(trustProxy)) { + issues.push({ + path: 'server.trustProxy', + message: 'server.trustProxy 仅支持 true/false、非负整数或 loopback/uniquelocal/linklocal', + }) + } requireInteger(issues, 'database.maxConnections', config.database?.maxConnections, { min: 1 }) requireOptionalInteger(issues, 'database.idleTimeoutMs', config.database?.idleTimeoutMs, { min: 1, @@ -288,6 +295,12 @@ function requireOptionalInteger( requireInteger(issues, configPath, value, options) } +function isValidTrustProxy(value: unknown): boolean { + if (typeof value === 'boolean') return true + if (typeof value === 'number') return Number.isInteger(value) && value >= 0 + return ['loopback', 'uniquelocal', 'linklocal'].includes(String(value)) +} + function isHttpUrl(value: string): boolean { try { const url = new URL(value) diff --git a/apps/backend/src/types/runtime-config.ts b/apps/backend/src/types/runtime-config.ts index d3f5bfb1..e0a2e873 100644 --- a/apps/backend/src/types/runtime-config.ts +++ b/apps/backend/src/types/runtime-config.ts @@ -20,6 +20,8 @@ export type AffiliateDashSkuMapping = Record export type RuntimeConfig = { server: { port: number + /** Express trust proxy:true/false、反代层数或 loopback 等预定义子网;默认 1(单层反代)。 */ + trustProxy?: boolean | number | string } data: { root: string diff --git a/apps/frontend/src/pages/worker/WorkerLoginPage.tsx b/apps/frontend/src/pages/worker/WorkerLoginPage.tsx index 2441ffa0..e525a32c 100644 --- a/apps/frontend/src/pages/worker/WorkerLoginPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerLoginPage.tsx @@ -40,6 +40,8 @@ import { hasWorkerSession, setWorkerSession } from '@/utils/worker-auth' import { isWorkerPasswordStrong, WORKER_PASSWORD_RULE_MESSAGE } from '@/utils/worker-password' const SMS_COUNTDOWN_SECONDS = 60 +/** 发送失败也冷却一段时间再允许重试,避免连续点击触发服务端每分钟发送次数限制。 */ +const SMS_FAILURE_COUNTDOWN_SECONDS = 15 const brandFeatures = [ { @@ -147,6 +149,7 @@ export default function WorkerLoginPage() { : `验证码已发送,${response.data.expiresInSeconds} 秒内有效`, ) } catch (error) { + setLoginSmsCountdown(SMS_FAILURE_COUNTDOWN_SECONDS) message.error(error instanceof Error ? error.message : '验证码发送失败') } finally { setLoading(false) @@ -217,6 +220,7 @@ export default function WorkerLoginPage() { : `验证码已发送,${response.data.expiresInSeconds} 秒内有效`, ) } catch (error) { + setCountdown(SMS_FAILURE_COUNTDOWN_SECONDS) message.error(error instanceof Error ? error.message : '验证码发送失败') } finally { setLoading(false) @@ -239,6 +243,7 @@ export default function WorkerLoginPage() { : `验证码已发送,${response.data.expiresInSeconds} 秒内有效`, ) } catch (error) { + setResetCountdown(SMS_FAILURE_COUNTDOWN_SECONDS) message.error(error instanceof Error ? error.message : '验证码发送失败') } finally { setLoading(false) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 1d66fa29..333fdd71 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -80,6 +80,7 @@ services: environment: TZ: ${TZ:-Asia/Shanghai} PORT: ${BACKEND_PORT:-3000} + HTTP_TRUST_PROXY: ${HTTP_TRUST_PROXY:-1} DATABASE_URL: ${DATABASE_URL:-postgres://postgres:postgres@postgres:5432/order_site} DATABASE_SSL: ${DATABASE_SSL:-false} DATABASE_MAX_CONNECTIONS: ${DATABASE_MAX_CONNECTIONS:-10} diff --git a/docker-compose.yml b/docker-compose.yml index 124be1fa..77e13d41 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -61,6 +61,7 @@ services: environment: TZ: ${TZ:-Asia/Shanghai} PORT: ${BACKEND_PORT:-3000} + HTTP_TRUST_PROXY: ${HTTP_TRUST_PROXY:-1} DATABASE_URL: ${DATABASE_URL:-postgres://postgres:postgres@postgres:5432/order_site} DATABASE_SSL: ${DATABASE_SSL:-false} DATABASE_MAX_CONNECTIONS: ${DATABASE_MAX_CONNECTIONS:-8}