feat: support Aliyun PNVS SMS verification

This commit is contained in:
Codex
2026-07-03 22:34:19 +08:00
parent 8ebe505dba
commit 69b3d80621
11 changed files with 441 additions and 59 deletions

View File

@@ -26,7 +26,7 @@ Supabase Auth 可继续作为最终 JWT 用户体系目标;本地重构期先
## 当前已实现
- `POST /api/auth/sms/send`:手机号验证码发送,验证码只保存 HMAC hash
- `POST /api/auth/sms/send`:手机号验证码发送。传统短信 provider 的验证码只保存 HMAC hash阿里云 PNVS 短信认证 provider 由阿里云生成并核验验证码,本地只保存发送流水、`outId`、频控和审计
- `POST /api/auth/sms/verify`:验证码登录,自动创建或复用 `platform_users`
- `GET /api/auth/me`:通过 Bearer token 获取当前用户。
- `POST /api/auth/logout`:吊销迁移期 session。
@@ -38,6 +38,7 @@ Supabase Auth 可继续作为最终 JWT 用户体系目标;本地重构期先
- `auth_login_events`:登录事件审计。
- `app_private.auth_sessions`:迁移期 session token hash。
- 阿里云短信 `SendSms` provider使用租户级 AccessKey、签名、模板发送。
- 阿里云 PNVS 短信认证 `SendSmsVerifyCode` / `CheckSmsVerifyCode` provider使用租户级 AccessKey、签名、模板发送和核验验证码适合不想走传统短信模板资质链路的手机号登录场景。
- 腾讯云短信 `SendSms` provider使用租户级 SecretId/SecretKey、SdkAppId、签名、模板发送。
- `POST /api/commerce/payments/create`:按订单创建微信支付 JSAPI 或支付宝 WAP 支付参数。
- `POST /api/commerce/payments/notify/wechat_pay`:微信支付 API v3 通知验签、AES-GCM 解密、幂等落库和权益开通。
@@ -45,11 +46,12 @@ Supabase Auth 可继续作为最终 JWT 用户体系目标;本地重构期先
## 短信 Provider
本地默认是 `AUTH_SMS_PROVIDER=mock`,仅开发环境返回 `debugCode`。生产环境只允许 `aliyun/aliyun-sms``tencent/tencent-sms`;如果仍为 mock 或写成未知 providerAPI 启动和 `readiness:production` 都会直接失败。
本地默认是 `AUTH_SMS_PROVIDER=mock`,仅开发环境返回 `debugCode`。生产环境只允许 `aliyun/aliyun-sms``aliyun-pnvs/aliyun-sms-auth``tencent/tencent-sms`;如果仍为 mock 或写成未知 providerAPI 启动和 `readiness:production` 都会直接失败。
真实 provider
- `aliyun` / `aliyun-sms`:阿里云短信 `SendSms`,需要 AccessKey、签名、模板 ID。
- `aliyun-pnvs` / `aliyun-sms-auth`:阿里云 PNVS 短信认证服务,调用 `SendSmsVerifyCode` 发送,调用 `CheckSmsVerifyCode` 核验;验证码由阿里云生成并校验,本地不保存明文验证码。
- `tencent` / `tencent-sms`:腾讯云短信 `SendSms`,需要 SecretId、SecretKey、SdkAppId、签名、模板 ID。
密钥策略:
@@ -60,6 +62,88 @@ Supabase Auth 可继续作为最终 JWT 用户体系目标;本地重构期先
- provider endpoint 默认使用官方域名,生产环境只允许 HTTPS 官方域名;本地测试可使用 `localhost/127.0.0.1` fake server。
- `readiness:production:db` 会检查 active/testing 短信 provider 的 `signName/templateCode``smsSdkAppId/signName/templateId`,并阻断公开配置里的 secret-like 字段。
### 阿里云 PNVS 短信认证配置示例
`AUTH_SMS_PROVIDER` 可设为 `aliyun-pnvs`,租户公开配置写 `tenant_auth_providers.config_public`AccessKey 写 `app_private.tenant_secrets`。PNVS 模板参数推荐使用阿里云占位 `##code##`,如 `{"code":"##code##","min":"5"}`;后端会用 `AUTH_CODE_TTL_SECONDS` 自动补 `min`
```json
{
"provider": "aliyun-pnvs",
"status": "active",
"configPublic": {
"signName": "工学教育",
"templateCode": "SMS_123456789",
"endpoint": "https://dypnsapi.aliyuncs.com",
"regionId": "cn-hangzhou",
"templateParam": {
"code": "##code##",
"min": "5"
},
"codeLength": "6",
"validTime": "300",
"interval": "60",
"duplicatePolicy": "1",
"secretRef": "app_private.tenant_secrets:sms:aliyun-pnvs"
},
"secret": {
"secretScope": "sms",
"secretKey": "aliyun-pnvs",
"secretJson": {
"accessKeyId": "LTAI********",
"accessKeySecret": "********"
}
}
}
```
生产 bootstrap SQL 参考:
```sql
insert into app_private.tenant_secrets (
tenant_id, secret_scope, secret_key, provider, secret_json, last_rotated_at
)
values (
:'tenant_id'::uuid,
'sms',
'aliyun-pnvs',
'aliyun-pnvs',
jsonb_build_object('accessKeyId', :'access_key_id', 'accessKeySecret', :'access_key_secret'),
now()
)
on conflict (tenant_id, secret_scope, secret_key)
do update set provider = excluded.provider,
secret_json = excluded.secret_json,
last_rotated_at = now(),
updated_at = now();
insert into public.tenant_auth_providers (
tenant_id, provider, status, display_name, config_public
)
values (
:'tenant_id'::uuid,
'aliyun-pnvs',
'active',
'阿里云短信认证',
jsonb_build_object(
'signName', :'sign_name',
'templateCode', :'template_code',
'endpoint', 'https://dypnsapi.aliyuncs.com',
'regionId', 'cn-hangzhou',
'templateParam', jsonb_build_object('code', '##code##', 'min', '5'),
'codeLength', '6',
'validTime', '300',
'interval', '60',
'duplicatePolicy', '1',
'secretRef', 'app_private.tenant_secrets:sms:aliyun-pnvs'
)
)
on conflict (tenant_id, provider)
do update set status = excluded.status,
display_name = excluded.display_name,
config_public = excluded.config_public,
updated_at = now();
```
### 阿里云短信配置示例
```json

View File

@@ -113,7 +113,7 @@ npm run smoke:launch-persona -- --write docs/refactor/launch-artifacts/launch-pe
ALLOW_LEGACY_AUTH_HEADERS=false
ALLOW_PLATFORM_ADMIN_KEY=false
CORS_ORIGIN=https://student.example.com,https://tenant-admin.example.com,https://platform-admin.example.com
AUTH_SMS_PROVIDER=aliyun 或 tencent
AUTH_SMS_PROVIDER=aliyun-pnvs、aliyun 或 tencent
```
生产 worker 推荐: