From ae491f592b2646142ff5ecbbb16f62a63dc9b146 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 4 Jul 2026 00:32:31 +0800 Subject: [PATCH] fix: validate PNVS provider rows for alias env --- docs/refactor/backend-capability-status.md | 2 +- scripts/deploy/README.md | 2 +- scripts/production-readiness-check-test.js | 20 +++++++++++++++++++- scripts/production-readiness-check.js | 10 +++++++--- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/docs/refactor/backend-capability-status.md b/docs/refactor/backend-capability-status.md index 5d4dbe4c..c64e4f75 100644 --- a/docs/refactor/backend-capability-status.md +++ b/docs/refactor/backend-capability-status.md @@ -36,7 +36,7 @@ | 能力 | 状态 | 说明 | | --- | --- | --- | -| 短信验证码登录 | 可联调 | 已有验证码、冷却、登录事件;推荐生产使用阿里云 PNVS 短信认证 `aliyun-pnvs`,由阿里云生成和核验验证码,本地只保存发送流水/`outId`/审计;传统阿里云短信、腾讯云短信和开发 mock provider 保留兼容,生产仍需跑真实手机号远程 smoke | +| 短信验证码登录 | 可联调 | 已有验证码、冷却、登录事件;生产统一使用阿里云 PNVS 短信认证 `aliyun-pnvs`,由阿里云生成和核验验证码,本地只保存发送流水/`outId`/审计;传统阿里云短信、腾讯云短信和开发 mock provider 保留兼容但不作为上线路径,生产仍需跑真实手机号远程 smoke | | 迁移期 session | 迁移期 | `tk_` token hash 存在 `app_private.auth_sessions`,用户态接口已优先解析 bearer session 并拒绝伪造 userId/tenantId | | Supabase Auth JWT | 可联调 | API 已用 Bearer JWT 验签并通过 `auth.users.id -> platform_users.auth_user_id -> tenant_memberships` 映射业务身份;支持 HS256 JWT secret 或 JWKS;测试覆盖学生、租户管理员、平台管理员、错租户、坏签名 | | 微信小程序登录 | 可联调 | `/api/auth/oauth/wechat-miniapp` 已接 `code2Session`、openid/unionid 身份、session 签发和登录审计 | diff --git a/scripts/deploy/README.md b/scripts/deploy/README.md index d385d57c..e68be5a3 100644 --- a/scripts/deploy/README.md +++ b/scripts/deploy/README.md @@ -154,7 +154,7 @@ Taro H5 构建存在 webpack asset size warning,这是前端包体优化事项 - 生产 API 已能启动,`https://api.tjszsb.com/api/tenant/resolve?host=app.tjszsb.com` 已返回 `master` 租户。 - Supabase self-hosted 运行在 `/opt/tiku-saas/supabase-project`,Kong 通过 Nginx 暴露到 `https://supabase.tjszsb.com`。 - 线上 H5 公开配置文件在 `/srv/tiku-saas/www/*/runtime-config.json`,密钥只允许放 `supabasePublishableKey` 这类公开 key。 -- 短信登录推荐生产使用 `AUTH_SMS_PROVIDER=aliyun-pnvs`。阿里云 AccessKey/Secret 只写入 `app_private.tenant_secrets(secret_scope='sms', secret_key='aliyun-pnvs')`,不要写进 `/etc/tiku-saas/api.env` 或 H5 `runtime-config.json`。 +- 短信验证码登录生产必须使用 `AUTH_SMS_PROVIDER=aliyun-pnvs`。阿里云 AccessKey/Secret 只写入 `app_private.tenant_secrets(secret_scope='sms', secret_key='aliyun-pnvs')`,不要写进 `/etc/tiku-saas/api.env` 或 H5 `runtime-config.json`。 配置 PNVS provider 推荐用仓库脚本写入数据库,避免手写 SQL 时把密钥打进命令历史。生产环境建议临时关闭 shell history,再用 `read -s` 输入 AccessKeySecret: diff --git a/scripts/production-readiness-check-test.js b/scripts/production-readiness-check-test.js index 1b331679..5ccf1bc6 100644 --- a/scripts/production-readiness-check-test.js +++ b/scripts/production-readiness-check-test.js @@ -219,10 +219,28 @@ WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.gongxue100.com/api/scan WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=s3cure-asset-scanner-token-2026-06-29-stuvwx WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false -`); +`, { + providerRows: [ + { + source: 'auth', + tenantId: 'tenant-pnvs', + provider: 'aliyun-pnvs', + configPublic: { + signName: '短信签名', + templateCode: 'SMS_123456789', + endpoint: 'https://dypnsapi.aliyuncs.com', + templateParam: { code: '##code##', min: '5' }, + }, + }, + ], +}); assert.equal(safeAliyunPnvsUnderscoreAlias.status, 0, `aliyun_pnvs readiness should pass without blockers: ${safeAliyunPnvsUnderscoreAlias.stdout} ${safeAliyunPnvsUnderscoreAlias.stderr}`); assert.equal(safeAliyunPnvsUnderscoreAlias.payload.summary?.blocker, 0, 'aliyun_pnvs readiness should have no blockers'); +assert.ok( + safeAliyunPnvsUnderscoreAlias.payload.checks?.some(item => item.id === 'db.auth_sms_provider_configured' && item.status === 'pass'), + 'aliyun_pnvs readiness should still validate matching PNVS provider rows', +); const pnvsTemplateParamWarning = runReadiness( ` diff --git a/scripts/production-readiness-check.js b/scripts/production-readiness-check.js index ff216224..d27276ed 100644 --- a/scripts/production-readiness-check.js +++ b/scripts/production-readiness-check.js @@ -160,19 +160,23 @@ function normalizeProvider(value) { return String(value || '').trim().toLowerCase(); } +function normalizeSmsProvider(value) { + return normalizeProvider(value).replace(/[_\s]/g, '-'); +} + function providerIn(provider, aliases) { return aliases.has(normalizeProvider(provider)); } function currentSmsProviderAliases() { - const provider = normalizeProvider(env('AUTH_SMS_PROVIDER', 'mock')).replace(/_/g, '-'); + const provider = normalizeSmsProvider(env('AUTH_SMS_PROVIDER', 'mock')); if (AUTH_PROVIDER_ALIASES.aliyunPnvs.has(provider)) return { provider: 'aliyun-pnvs', aliases: AUTH_PROVIDER_ALIASES.aliyunPnvs }; return { provider, aliases: new Set([provider]) }; } function validateActiveSmsProviderRows(rows, sourceLabel = 'database') { const expected = currentSmsProviderAliases(); - if (!PRODUCTION_SMS_PROVIDERS.has(normalizeProvider(env('AUTH_SMS_PROVIDER', 'mock')))) return; + if (!PRODUCTION_SMS_PROVIDERS.has(normalizeSmsProvider(env('AUTH_SMS_PROVIDER', 'mock')))) return; const activeRows = rows.filter(row => row.source === 'auth' && providerIn(row.provider, expected.aliases)); if (activeRows.length === 0) { block('db.auth_sms_provider_configured', 'AUTH_SMS_PROVIDER must have a matching active/testing tenant_auth_providers row', { @@ -565,7 +569,7 @@ function validateEnv() { } } - const authSmsProvider = normalizeProvider(env('AUTH_SMS_PROVIDER', 'mock')).replace(/[_\s]/g, '-'); + const authSmsProvider = normalizeSmsProvider(env('AUTH_SMS_PROVIDER', 'mock')); if (!PRODUCTION_SMS_PROVIDERS.has(authSmsProvider)) { block('env.auth_sms_provider', 'AUTH_SMS_PROVIDER must be aliyun-pnvs in production', { provider: authSmsProvider || '(empty)',