- 新增 POST /auth/fusion/token 获取 SDK 鉴权 Token - 新增 POST /auth/fusion/verify 用 verifyToken 换取手机号并登录/注册 - 新增 GET /auth/providers 按平台返回可用登录方式列表 - 新增 PUT /admin/auth-providers 管理端热切换第三方登录开关 - 新增 appSettings 表存储运行时配置,支持不重启生效 - 修复 schema 中超长外键名称导致的 db:push 失败
423 lines
16 KiB
Markdown
423 lines
16 KiB
Markdown
# 阿里云融合认证集成 — 设计与实施计划
|
||
|
||
> Phase 1e: Phone Number Authentication
|
||
> Created: 2026-05-27
|
||
> Status: Implemented
|
||
|
||
## 概述
|
||
|
||
集成阿里云号码认证服务的**融合认证**方案,为多奇平台提供手机号一键登录能力。融合认证是一种校验用户手机号的云原生服务,集成了号码认证(运营商网关取号)、短信验证码、语音验证码等通信功能。
|
||
|
||
### 目标
|
||
|
||
- 新增 `POST /auth/fusion/token` 端点,为客户端 SDK 提供鉴权 Token(对齐 Flutter 客户端 `FUSION_AUTH_INTEGRATION.md` 第 5.1 节)
|
||
- 新增 `POST /auth/fusion/verify` 端点,用运营商 verifyToken 换取手机号并完成登录(对齐 Flutter 客户端第 5.2 节)
|
||
- 支持游客账号通过手机号关联升级为正式账号
|
||
|
||
### 不在范围内
|
||
|
||
- 短信验证码/语音验证码登录(号码认证降级到短信时由阿里云模板自动处理,服务端感知不变)
|
||
- Flutter 客户端 SDK 集成(客户端侧独立进行,见 `duoqi-flutter/docs/FUSION_AUTH_INTEGRATION.md`)
|
||
- 修改现有 `POST /auth/phone`(保留 501 占位,未来可用于非融合认证的手机登录)
|
||
|
||
---
|
||
|
||
## 架构分析
|
||
|
||
### 现有认证架构
|
||
|
||
项目已实现 `guest`、`huawei`、`apple` 三种登录方式,采用统一的 `findOrCreate*` 模式:
|
||
|
||
```
|
||
客户端 ──▶ 路由层(Zod校验) ──▶ 服务层(业务逻辑) ──▶ 数据库(users表)
|
||
│
|
||
signTokens() + buildLoginResponse()
|
||
```
|
||
|
||
关键设计:
|
||
- `users` 表以 `(authType, authId)` 唯一索引标识用户
|
||
- `authType` 枚举已包含 `'phone'`,`authId` 用于存储手机号
|
||
- JWT payload 携带 `{ userId, authType, tier }`
|
||
- 登录响应统一为 `{ user, tokens }` 结构
|
||
|
||
### 融合认证集成点
|
||
|
||
融合认证引入一个**两步交互**流程,比现有的华为/Apple 认证多一个"预取 Token"步骤:
|
||
|
||
```
|
||
┌──────────┐ ┌──────────────┐ ┌──────────────────┐
|
||
│ 客户端 │ │ duoqi-api │ │ 阿里云 Dypnsapi │
|
||
└────┬─────┘ └──────┬───────┘ └────────┬─────────┘
|
||
│ │ │
|
||
│ ① POST /auth/fusion/token │
|
||
│ { platform, packageName?, bundleId? } │
|
||
│───────────────────▶│ │
|
||
│ │ ② GetFusionAuthToken │
|
||
│ │────────────────────────▶│
|
||
│ │◀── { authToken } ────────│
|
||
│◀── { fusionAuthToken } ──│ │
|
||
│ │ │
|
||
│ [客户端 SDK 使用 authToken 完成认证] │
|
||
│ [获得 verifyToken] │ │
|
||
│ │ │
|
||
│ ③ POST /auth/fusion/verify │
|
||
│ { verifyToken } │ │
|
||
│───────────────────▶│ │
|
||
│ │ ④ VerifyWithFusionAuthToken
|
||
│ │────────────────────────▶│
|
||
│ │◀── { phoneNumber, verifyResult, phoneScore }
|
||
│ │ │
|
||
│ │ ⑤ 查找或创建用户,签发JWT │
|
||
│◀── { user, tokens } │ │
|
||
│ │ │
|
||
```
|
||
|
||
> 路由路径对齐 Flutter 客户端 `FUSION_AUTH_INTEGRATION.md` 第 5 节的定义。
|
||
|
||
---
|
||
|
||
## 关键设计决策
|
||
|
||
### 决策 1:手机号存储格式
|
||
|
||
**选择:存储阿里云返回的原始手机号字符串(如 `18012341234`)**
|
||
|
||
理由:
|
||
- `VerifyWithFusionAuthToken` 在号码认证通过时返回完整手机号(非掩码)
|
||
- 完整手机号作为 `authId` 可确保用户唯一性
|
||
- 掩码号(如 `180****1234`)仅出现在日志或审计场景,不用于身份标识
|
||
|
||
替代方案(未采纳):
|
||
- 存储掩码号:无法唯一标识用户,不可行
|
||
- 对手机号做哈希:丧失可读性,且哈希碰撞风险虽极低但非零
|
||
|
||
### 决策 2:authToken 有效期
|
||
|
||
**选择:900 秒(15 分钟,API 允许的最小值)**
|
||
|
||
理由:
|
||
- 融合认证的典型交互在几秒到几十秒内完成
|
||
- 更短的 Token 有效期意味着更小的被盗用风险窗口
|
||
- 与项目中 access_token 1 小时、refresh_token 30 天的分级策略一致——authToken 作为临时凭证应有最短有效期
|
||
|
||
### 决策 3:阿里云 SDK 客户端生命周期
|
||
|
||
**选择:懒初始化单例模式**
|
||
|
||
理由:
|
||
- 阿里云 SDK 客户端是线程安全的,可复用
|
||
- 避免每次请求创建新客户端的开销(HTTP 连接池复用)
|
||
- 与项目中 `db` 客户端的单例模式保持一致
|
||
- 仅在配置了阿里云环境变量时才初始化,不影响其他认证方式
|
||
|
||
### 决策 4:路由设计
|
||
|
||
**选择:新增 `/auth/fusion/token` 和 `/auth/fusion/verify`,独立于 `/auth/phone`**
|
||
|
||
理由:
|
||
- Flutter 客户端已在 `FUSION_AUTH_INTEGRATION.md` 第 5 节明确定义了这两个路径,服务端需对齐
|
||
- `/auth/fusion/` 作为独立命名空间,语义上更准确——融合认证不只是一键取号,而是由阿里云模板编排的多种认证方式(号码认证、短信、图形验证等)
|
||
- `/auth/phone` 保留 501 占位,未来可用于非融合认证的原生短信验证码登录等场景
|
||
|
||
替代方案(未采纳):
|
||
- `POST /auth/phone/fusion-token` + 修改 `POST /auth/phone`:路径层级不合理,融合认证不等同于"手机号登录"
|
||
- 在 `/auth/phone` 请求体中加 `action: 'getToken' | 'verify'`:语义不够明确,增加请求体复杂度
|
||
|
||
### 决策 5:账号关联扩展
|
||
|
||
**选择:在 `/auth/link` 中支持 `provider: 'phone'`**
|
||
|
||
理由:
|
||
- 已有游客关联华为/Apple 的完整流程(`account-link-service.ts`)
|
||
- 手机号关联复用相同的 `accountMigrations` 表和幂等逻辑
|
||
- 仅需扩展 `linkSchema` 的 `provider` 枚举和 `credential` 结构
|
||
|
||
---
|
||
|
||
## 阿里云 API 参考
|
||
|
||
### GetFusionAuthToken
|
||
|
||
| 项目 | 值 |
|
||
|------|------|
|
||
| Action | `GetFusionAuthToken` |
|
||
| 授权 | `dypns:GetFusionAuthToken` |
|
||
| Endpoint | `dypnsapi.aliyuncs.com` |
|
||
|
||
**请求参数:**
|
||
|
||
| 名称 | 类型 | 必填 | 描述 | 示例值 |
|
||
|------|------|------|------|--------|
|
||
| SchemeCode | string | 是 | 认证方案 Code | `FA1000*************201` |
|
||
| Platform | string | 是 | 平台:`Android` 或 `iOS` | `Android` |
|
||
| PackageName | string | Android 必填 | App 包名 | `com.example.test` |
|
||
| PackageSign | string | Android 必填 | App 包签名 | `47fcc************************278` |
|
||
| BundleId | string | iOS 必填 | App bundleId | `com.example.test` |
|
||
| DurationSeconds | long | 是 | Token 有效时长(秒),范围 900–43200 | `900` |
|
||
|
||
**响应示例:**
|
||
|
||
```json
|
||
{
|
||
"Message": "成功",
|
||
"RequestId": "CC3BB6D2-2FDF-4321-9DCE-B38165CE4C47",
|
||
"Model": "FKcksloqk***********jalEc+",
|
||
"Code": "OK",
|
||
"Success": true
|
||
}
|
||
```
|
||
|
||
### VerifyWithFusionAuthToken
|
||
|
||
| 项目 | 值 |
|
||
|------|------|
|
||
| Action | `VerifyWithFusionAuthToken` |
|
||
| 授权 | `dypns:VerifyWithFusionAuthToken` |
|
||
| Endpoint | `dypnsapi.aliyuncs.com` |
|
||
|
||
**请求参数:**
|
||
|
||
| 名称 | 类型 | 必填 | 描述 | 示例值 |
|
||
|------|------|------|------|--------|
|
||
| VerifyToken | string | 是 | 客户端 SDK 完成认证后返回的 Token | `LD108enNdlsl*******sFLKCks1==` |
|
||
|
||
**响应示例:**
|
||
|
||
```json
|
||
{
|
||
"Message": "示例值",
|
||
"RequestId": "CC3BB6D2-2FDF-4321-9DCE-B38165CE4C47",
|
||
"Model": {
|
||
"PhoneNumber": "18012341234",
|
||
"VerifyResult": "PASS",
|
||
"PhoneScore": 20
|
||
},
|
||
"Code": "OK",
|
||
"Success": true
|
||
}
|
||
```
|
||
|
||
**关键错误码:**
|
||
|
||
| HTTP 状态码 | 错误码 | 描述 |
|
||
|-------------|--------|------|
|
||
| 400 | `SmsCodeVerifyFail` | 短信验证码失败(号码认证降级到短信时) |
|
||
| 400 | `Throttling.System` | 接口被限流 |
|
||
| 400 | `VerifySchemeNotExist` | 认证方案不存在 |
|
||
| 400 | `SchemeNotPassed` | 认证方案未通过审核 |
|
||
| 400 | `Unsupported.Account` | 账号未开通号码认证服务 |
|
||
| 403 | `UnauthorizedOperation` | 权限校验失败 |
|
||
| 500 | `SystemError` | 系统异常 |
|
||
|
||
---
|
||
|
||
## 分步实施计划
|
||
|
||
### Phase 1:基础设施(前置条件)
|
||
|
||
| # | 任务 | 产出文件 | 验证标准 |
|
||
|---|------|----------|----------|
|
||
| 1.1 | 安装阿里云 SDK 依赖 | `package.json` | `@alicloud/dypnsapi20170525`、`@alicloud/openapi-client`、`@alicloud/tea-util` 安装成功 |
|
||
| 1.2 | 添加环境变量 | `src/utils/config.ts`、`.env.example` | `ALIYUN_ACCESS_KEY_ID`、`ALIYUN_ACCESS_KEY_SECRET`、`ALIYUN_FUSION_SCHEME_CODE` 加入 Zod schema,可选字段 |
|
||
| 1.3 | 类型检查通过 | — | `bun run typecheck` 无错误 |
|
||
|
||
### Phase 2:服务层
|
||
|
||
| # | 任务 | 产出文件 | 验证标准 |
|
||
|---|------|----------|----------|
|
||
| 2.1 | 创建阿里云客户端封装 | `src/services/auth/fusion-auth-client.ts`(新建) | 懒初始化单例,配置从 `config` 读取 |
|
||
| 2.2 | 实现 `getFusionAuthToken` | `src/services/auth/fusion-auth-client.ts` | 接收平台参数,调用阿里云 API,返回 Token 字符串 |
|
||
| 2.3 | 实现 `verifyWithFusionAuthToken` | `src/services/auth/fusion-auth-client.ts` | 接收 verifyToken,调用阿里云 API,返回 `{ phoneNumber, verifyResult }` |
|
||
| 2.4 | 实现 `findOrCreatePhone` | `src/services/auth/jwt.ts` | 遵循 `findOrCreateGuest` / `findOrCreateHuawei` 相同模式 |
|
||
| 2.5 | 类型检查通过 | — | `bun run typecheck` 无错误 |
|
||
|
||
### Phase 3:路由层
|
||
|
||
| # | 任务 | 产出文件 | 验证标准 |
|
||
|---|------|----------|----------|
|
||
| 3.1 | 新增 `POST /auth/fusion/token` 路由 | `src/routes/auth.ts` | 接收 `{ platform, packageName?, packageSign?, bundleId? }`,调用 `getFusionAuthToken`,返回 `{ fusionAuthToken }` |
|
||
| 3.2 | 新增 `POST /auth/fusion/verify` 路由 | `src/routes/auth.ts` | 接收 `{ verifyToken }`,调用 `verifyWithFusionAuthToken` + `findOrCreatePhone`,返回标准 `LoginResponse` |
|
||
| 3.3 | 扩展 `/auth/link` 支持 phone provider | `src/routes/auth.ts`、`src/services/auth/account-link-service.ts` | `linkSchema.provider` 增加 `'phone'`,credential 结构适配 |
|
||
| 3.4 | 类型检查通过 | — | `bun run typecheck` 无错误 |
|
||
|
||
### Phase 4:测试
|
||
|
||
| # | 任务 | 产出文件 | 验证标准 |
|
||
|---|------|----------|----------|
|
||
| 4.1 | fusion-auth-client 单元测试 | `src/__tests__/services/fusion-auth.test.ts`(新建) | mock 阿里云 SDK,测试正常流程和错误处理 |
|
||
| 4.2 | findOrCreatePhone 单元测试 | `src/__tests__/services/auth.test.ts` | 测试新用户创建和已有用户查找 |
|
||
| 4.3 | 全量测试通过 | — | `bun run test` 全部通过 |
|
||
|
||
### Phase 5:文档
|
||
|
||
| # | 任务 | 产出文件 | 验证标准 |
|
||
|---|------|----------|----------|
|
||
| 5.1 | 更新 API 文档 | `docs/api-reference.md` | 新增 `POST /auth/fusion/token` 和 `POST /auth/fusion/verify` 端点描述 |
|
||
| 5.2 | 更新 CLAUDE.md 项目结构 | `CLAUDE.md` | services/auth 目录列表新增 `fusion-auth-client` |
|
||
|
||
---
|
||
|
||
## 新增环境变量
|
||
|
||
```bash
|
||
# .env 新增项(阿里云融合认证)
|
||
ALIYUN_ACCESS_KEY_ID= # 阿里云 RAM 用户的 AccessKey ID
|
||
ALIYUN_ACCESS_KEY_SECRET= # 阿里云 RAM 用户的 AccessKey Secret
|
||
ALIYUN_FUSION_SCHEME_CODE= # 融合认证方案 Code(在阿里云控制台创建)
|
||
```
|
||
|
||
配置特点:
|
||
- 三个变量均为可选——未配置时不影响游客、华为、Apple 等其他登录方式
|
||
- 调用融合认证相关接口时检查配置,未配置则返回 `NOT_IMPLEMENTED` 或 `SERVICE_UNAVAILABLE`
|
||
- AccessKey 通过 RAM 子用户授权,仅需 `AliyunDypnsFullAccess` 权限
|
||
|
||
---
|
||
|
||
## 新增 API 端点定义
|
||
|
||
> 路由路径与 Flutter 客户端 `FUSION_AUTH_INTEGRATION.md` 第 5 节保持一致。
|
||
|
||
### POST /auth/fusion/token
|
||
|
||
认证:无
|
||
限流:10 次/分钟
|
||
|
||
用途:为客户端融合认证 SDK 提供鉴权 Token。客户端在调用 `FusionAuth.init(schemeCode, authToken)` 时需要此 Token。对应阿里云 `GetFusionAuthToken` API。
|
||
|
||
请求:
|
||
|
||
```json
|
||
{
|
||
"platform": "Android",
|
||
"packageName": "com.duoqi.app",
|
||
"packageSign": "47fcc************************278"
|
||
}
|
||
```
|
||
|
||
```json
|
||
{
|
||
"platform": "iOS",
|
||
"bundleId": "com.duoqi.app"
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `platform` | `'Android' \| 'iOS'` | 是 | 客户端平台 |
|
||
| `packageName` | string | Android 必填 | App 包名 |
|
||
| `packageSign` | string | Android 必填 | App 包签名 |
|
||
| `bundleId` | string | iOS 必填 | App bundleId |
|
||
|
||
响应:
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"fusionAuthToken": "FKcksloqk***********jalEc+"
|
||
},
|
||
"error": null
|
||
}
|
||
```
|
||
|
||
错误响应(服务未配置):
|
||
|
||
```json
|
||
{
|
||
"success": false,
|
||
"data": null,
|
||
"error": {
|
||
"code": "SERVICE_UNAVAILABLE",
|
||
"message": "Fusion auth is not configured"
|
||
}
|
||
}
|
||
```
|
||
|
||
### POST /auth/fusion/verify
|
||
|
||
认证:无
|
||
限流:10 次/分钟
|
||
|
||
用途:客户端融合认证 SDK 完成认证后(`onVerifySuccess` 事件),将运营商返回的 verifyToken 发送到此端点。服务端用 verifyToken 向阿里云换取手机号,完成用户查找或创建,签发 JWT。对应阿里云 `VerifyWithFusionAuthToken` API。
|
||
|
||
请求:
|
||
|
||
```json
|
||
{
|
||
"verifyToken": "LD108enNdlsl*******sFLKCks1=="
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `verifyToken` | string | 是 | 客户端 SDK `onVerifySuccess` 事件返回的 Token |
|
||
|
||
成功响应(同 `/auth/guest`):
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"user": {
|
||
"id": "uuid",
|
||
"nickname": null,
|
||
"avatarUrl": null,
|
||
"tier": "free"
|
||
},
|
||
"tokens": {
|
||
"accessToken": "jwt",
|
||
"refreshToken": "jwt"
|
||
}
|
||
},
|
||
"error": null
|
||
}
|
||
```
|
||
|
||
错误响应(认证失败):
|
||
|
||
```json
|
||
{
|
||
"success": false,
|
||
"data": null,
|
||
"error": {
|
||
"code": "VALIDATION_ERROR",
|
||
"message": "Phone verification failed"
|
||
}
|
||
}
|
||
```
|
||
|
||
错误响应(服务未配置):
|
||
|
||
```json
|
||
{
|
||
"success": false,
|
||
"data": null,
|
||
"error": {
|
||
"code": "SERVICE_UNAVAILABLE",
|
||
"message": "Fusion auth is not configured"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 前置条件检查清单
|
||
|
||
在开始编码之前,需要以下准备工作:
|
||
|
||
- [ ] **阿里云 RAM 子用户**:创建 RAM 子用户,授予 `AliyunDypnsFullAccess` 权限,获取 AccessKey ID 和 Secret
|
||
- [ ] **融合认证方案**:在号码认证服务控制台创建融合认证方案,获取 SchemeCode
|
||
- [ ] **Android 签名信息**:确认 App 包名 (`PackageName`) 和签名 (`PackageSign`)
|
||
- [ ] **iOS Bundle 信息**:确认 App bundleId
|
||
- [ ] **Flutter 客户端状态**:确认 duoqi-flutter 是否已集成融合认证 SDK
|
||
|
||
---
|
||
|
||
## 风险与注意事项
|
||
|
||
1. **手机号隐私合规**:存储用户手机号需符合《个人信息保护法》,建议在隐私政策中明确告知
|
||
2. **运营商支持范围**:号码认证(一键取号)依赖运营商网关,WiFi 环境可能降级到短信验证码
|
||
3. **限流**:阿里云 API 有自身限流(错误码 `Throttling.System`),服务端应做好重试和降级
|
||
4. **错误码映射**:阿里云的错误码需映射到项目统一的 `error.code` 体系
|
||
5. **账号冲突**:同一手机号可能已通过其他方式注册(如先游客后手机号),需考虑合并策略
|