- 新增 POST /auth/fusion/token 获取 SDK 鉴权 Token - 新增 POST /auth/fusion/verify 用 verifyToken 换取手机号并登录/注册 - 新增 GET /auth/providers 按平台返回可用登录方式列表 - 新增 PUT /admin/auth-providers 管理端热切换第三方登录开关 - 新增 appSettings 表存储运行时配置,支持不重启生效 - 修复 schema 中超长外键名称导致的 db:push 失败
16 KiB
阿里云融合认证集成 — 设计与实施计划
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 |
响应示例:
{
"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== |
响应示例:
{
"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 |
新增环境变量
# .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。
请求:
{
"platform": "Android",
"packageName": "com.duoqi.app",
"packageSign": "47fcc************************278"
}
{
"platform": "iOS",
"bundleId": "com.duoqi.app"
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
platform |
'Android' | 'iOS' |
是 | 客户端平台 |
packageName |
string | Android 必填 | App 包名 |
packageSign |
string | Android 必填 | App 包签名 |
bundleId |
string | iOS 必填 | App bundleId |
响应:
{
"success": true,
"data": {
"fusionAuthToken": "FKcksloqk***********jalEc+"
},
"error": null
}
错误响应(服务未配置):
{
"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。
请求:
{
"verifyToken": "LD108enNdlsl*******sFLKCks1=="
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
verifyToken |
string | 是 | 客户端 SDK onVerifySuccess 事件返回的 Token |
成功响应(同 /auth/guest):
{
"success": true,
"data": {
"user": {
"id": "uuid",
"nickname": null,
"avatarUrl": null,
"tier": "free"
},
"tokens": {
"accessToken": "jwt",
"refreshToken": "jwt"
}
},
"error": null
}
错误响应(认证失败):
{
"success": false,
"data": null,
"error": {
"code": "VALIDATION_ERROR",
"message": "Phone verification failed"
}
}
错误响应(服务未配置):
{
"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
风险与注意事项
- 手机号隐私合规:存储用户手机号需符合《个人信息保护法》,建议在隐私政策中明确告知
- 运营商支持范围:号码认证(一键取号)依赖运营商网关,WiFi 环境可能降级到短信验证码
- 限流:阿里云 API 有自身限流(错误码
Throttling.System),服务端应做好重试和降级 - 错误码映射:阿里云的错误码需映射到项目统一的
error.code体系 - 账号冲突:同一手机号可能已通过其他方式注册(如先游客后手机号),需考虑合并策略