# 阿里云融合认证集成 — 设计与实施计划 > 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. **账号冲突**:同一手机号可能已通过其他方式注册(如先游客后手机号),需考虑合并策略