23 KiB
短信渠道插件与登录注册策略方案
状态:设计提案,未实施。本文不表示接口、数据库或页面已交付。
1. 目标与默认决策
- 支持多个短信渠道实例;同一供应商允许配置多个账号、应用或签名。
- 首批提供阿里云、腾讯云两个系统级官方短信插件,复用现有插件生命周期和配置 schema,不另建插件中心。
- 后台新增短信渠道、场景路由、发送记录;登录注册设置增加短信验证码登录、邮箱验证码登录、短信注册、邮箱注册、短信与邮箱双验证注册。
- “短信+邮箱注册”指两者都必须验证,不是任意一个。短信注册与邮箱注册同时开启则表示任选其一。双验证开启后与单验证注册互斥,防止绕过。
- 第一期只开放中国大陆手机号,内部统一为 E.164。国际短信、营销群发、随机负载均衡、公开第三方可执行短信插件不在本期。
- 保留现有密码登录与会话机制;注册暂保留现有用户名、密码和服务协议要求,不顺便改成无密码注册。验证码登录本身不要求密码。
- 短信发送是有费用的外部写操作。配置检查不发送短信;真实测试需要管理员明确触发并确认接收号码。
2. 当前实现与改造依据
仓库检查表明:
| 现状 | 代码依据(仓库相对位置) | 设计影响 |
|---|---|---|
| 密码登录支持用户名或邮箱;普通注册要求邮箱验证码,首个管理员例外 | backend/internal/auth/auth.go |
分离登录方法、注册策略与发送能力,不能只加页面开关 |
| 邮件验证码用于注册和找回密码,已有 HMAC 和事务消费 | backend/internal/auth/email.go、password_reset.go、backend/internal/repository/repository.go |
演进为统一挑战机制,保留安全能力,不复制 SMS 版邮件流程 |
| 用户没有手机号和联系方式验证时间 | backend/internal/model/models_identity.go |
新增手机与验证状态,历史邮箱不能直接视为已验证 |
| 非空邮箱已有大小写归一化唯一索引 | backend/internal/database/schema.go |
手机增加等价唯一约束,不重复建设现有邮箱唯一索引 |
| 支付插件已有系统级启停、host 执行、配置 schema、Registry 及可信进程 RPC | backend/internal/app/payment_plugins.go、plugin_management.go、backend/internal/payment/ |
复用插件宿主模式;不要把短信注册在模型 provider 或支付 registry 中 |
| 登录注册后台目前主要管理注册总开关和 Linux.do | web/src/pages/admin/components/access-settings-panel.tsx |
改为统一认证策略页面,邮件配置继续负责 SMTP |
| Linux.do 可自动创建新用户;运行平台已有 Redis 协调能力 | backend/internal/auth/linuxdo.go、backend/internal/platform/coordination.go |
新用户必须经过统一注册策略;多实例限流不能依赖进程 mutex |
工作区存在与本方案无关的用户修改。本次仅新增设计文档和索引,不触碰这些修改。
3. 架构和职责
HTTP handler
-> service 别名 -> app 组合根
-> auth:登录、注册、联系方式绑定、策略及会话
-> verification:验证码挑战、失败次数、有效期及消费条件
-> sms:路由、限额、发送尝试及状态对账
-> SMS Registry -> 官方插件适配器 -> 官方 Go SDK
-> 现有 SMTP 发信器
-> repository/model:查询、事务与持久化
-> platform:Redis/单机协调、定时对账
建议新增 backend/internal/sms/、backend/internal/verification/;供应商适配器放 backend/internal/sms/providers/aliyun/ 与 tencent/。注册事务由 repository 统一完成,验证码域不直接创建用户,短信域不决定谁能登录。
app 注入邮件与短信发送依赖,域包不反向 import app 或 service。新增前端业务模块 services/api/sms.ts,只使用项目 http 入口。
3.1 插件、渠道、模板、路由必须分开
| 对象 | 含义 | 示例 |
|---|---|---|
| 插件 | 一种供应商协议能力 | official-sms-aliyun、official-sms-tencent |
| 渠道实例 | 独立账号/应用、密钥、默认签名、启停、配额 | 阿里云主渠道、阿里云备用账号、腾讯云备用渠道 |
| 场景模板绑定 | 某渠道某场景的云端模板及变量映射 | 登录、注册、管理员测试 |
| 场景路由 | 首选渠道及明确有序的备用渠道 | 登录走阿里,明确拒绝时可尝试腾讯 |
新增 manifest 的 smsProviders contribution 和管理分类 sms;第一期使用系统官方 host-backed 执行模式。配置 schema 描述密钥、地区、应用和模板字段,宿主渲染渠道表单。密钥按渠道保存,而不是一个插件只能保存一份账号。
插件提供 Descriptor、ValidateConfig、Send,并以能力声明提供送达状态查询。宿主定义模板变量 code、ttlMinutes 等,再由适配器转换成供应商格式。插件不能创建验证码、登录用户、读取用户表或创建会话。
未来可信进程插件可使用独立的 yingce.sms/v1 ABI;本期不复制一套大型进程运行时。进程隔离不等于安全沙箱,不能据此允许上传任意不可信二进制。
4. SDK 参考基线
本次通过供应商官方 GitHub API 核对 tag、对应提交和模块文件(2026-09-22):
| 供应商 | 官方 SDK / API | 本次发现的最新正式 tag | tag 所指提交时间(UTC) |
|---|---|---|---|
| 阿里云 | github.com/alibabacloud-go/dysmsapi-20170525/v5,API 2017-05-25 |
v5.6.0 |
2026-07-01 13:57:25 |
| 腾讯云 | github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms,包 v20210111 |
SDK 仓库 v1.3.184 |
2026-09-21 18:47:20 |
阿里 tag 的 go.mod 声明 Go 1.14,依赖 darabonba-openapi/v2 v2.2.2 和 tea v1.5.1;腾讯 SMS 子模块声明 Go 1.14,并依赖 common v1.3.184。这只是模块声明,与项目 Go 1.25 的实际集成兼容性仍须编译验证。
正式 tag 不等于已经在本项目证明稳定。腾讯此版本刚发布,将其列为候选,不宣称经过长期生产验证。实施时重新核对正式 tag、变更记录和安全修复,通过模块解析、编译、契约测试及真实小流量验收后固定 go.mod/go.sum;不要使用浮动 latest 或 master。腾讯还须确认 SMS/common 子模块可解析版本匹配。
已查看官方模型源码:阿里发送使用签名、模板编码和 JSON 模板参数,返回包含 Code、BizId、RequestId;腾讯使用 SmsSdkAppId、TemplateId、TemplateParamSet,手机号支持 E.164,返回逐号码状态与流水号。适配器必须解析业务结果,而不只检查 Go error 或 HTTP 状态。
官方核对入口与固定版本源码:
https://api.github.com/repos/alibabacloud-go/dysmsapi-20170525/tags?per_page=10
https://github.com/alibabacloud-go/dysmsapi-20170525/tree/v5.6.0
https://api.github.com/repos/alibabacloud-go/dysmsapi-20170525/commits/v5.6.0
https://github.com/alibabacloud-go/dysmsapi-20170525/blob/v5.6.0/client/send_sms_request_model.go
https://github.com/alibabacloud-go/dysmsapi-20170525/blob/v5.6.0/client/send_sms_response_body_model.go
https://api.github.com/repos/TencentCloud/tencentcloud-sdk-go/tags?per_page=10
https://api.github.com/repos/TencentCloud/tencentcloud-sdk-go/commits/v1.3.184
https://github.com/TencentCloud/tencentcloud-sdk-go/blob/v1.3.184/tencentcloud/sms/go.mod
https://github.com/TencentCloud/tencentcloud-sdk-go/blob/v1.3.184/tencentcloud/sms/v20210111/models.go
SDK 负责签名与协议,不自行实现阿里或腾讯签名算法。发送调用配置明确超时、取消和受控网络出口;关闭或明确约束 SDK 自动重试,防止宿主与 SDK 双层重试。默认使用官方域名和地区枚举,不提供任意 endpoint 输入框;同样遵守项目出站安全策略。
5. 登录注册开关合同
5.1 管理界面
登录方式
密码登录 保留现有能力
开启短信验证码登录 独立开关
开启邮件验证码登录 独立开关
Linux.do 登录 保留现有配置
注册策略
开放新用户注册 总开关
开启短信验证码注册 单验证入口
开启邮件验证码注册 单验证入口
开启短信+邮箱注册 强制双验证,与上面两个入口互斥
持久化不是五个不受约束的布尔值:登录使用独立方法开关;注册使用总开关和 methods 集合,允许 []、[sms]、[email]、[sms,email] 或 [sms_email]。sms_email 不得与单验证方法同时出现。
开启双验证时,UI 明示将关闭单验证入口,服务端同样拒绝冲突配置。关闭双验证后要求明确选择单验证入口,不自动恢复隐含历史配置。
注册总开关关闭时,禁止发注册验证码、创建注册流程及最终提交;不影响已有用户登录。登录验证码不会为未知号码自动注册用户。注册方法与登录方法相互独立:开启短信注册不会隐式开启短信登录。
策略保存与最终提交均强校验依赖;渠道有启用实例、对应模板与路由,邮件有可用配置。检查只能证明本地配置完整,不承诺云端模板审核、余额或网络一定正常。公网配置只返回有效方法与可展示的不可用原因,不泄露供应商或密钥。
策略保存使用版本号防覆盖;认证流程绑定版本,最终提交重新校验当前策略,避免发送验证码后利用旧策略绕过管理员调整。
5.2 双验证注册流程
- 创建短期
auth_flow,方法为sms_email,绑定浏览器持有的不可猜测流程凭据。 - 输入手机号、邮箱;分别发送
register/sms与register/email验证码,挑战绑定该流程与归一化目标。 - 两个输入都有明确的倒计时、失效和错误反馈;不能在发送时就创建用户。
- 最终提交在一个数据库事务里校验并消费两个挑战、创建唯一用户及验证标记;任意失败均不能只消费其中一个。
- 注册结果/幂等键绑定流程,响应丢失后的重试不能创建第二个用户或重复发放赠送额度。会话创建与注册成功状态协调,通知发送不进入数据库长事务。
成功验证一项后修改手机号或邮箱,必须使对应验证结果失效。不得只拿两个不受流程绑定的 challengeId 任意拼接。
5.3 账号归属和已有流程
- 同手机号/邮箱的唯一性由数据库兜底;手机号与邮箱已分别属于两个账户时拒绝注册,不自动合并。
- 新注册的联系方式写入验证时间。历史邮箱、首个管理员可选邮箱、OAuth 返回邮箱不能一律补成已验证。
- 对无可信验证证据的已有账户,先通过原有登录方式进入安全设置,再完成邮箱/手机绑定验证;验证码登录只使用已验证联系方式,避免把历史填错的邮箱变成接管入口。
- 本期必须包含最小联系方式绑定入口,否则已有用户无法安全启用手机登录。绑定/换绑需要近期重新认证、新联系方式验证和唯一性检查;不凭新号码验证码直接替换既有账户归属。
- 首个管理员仍使用独立初始化路径,但创建权限必须由数据库事务/全局初始化状态保护,不依赖单进程用户计数和 mutex。
- Linux.do 已有用户登录不变;首次创建用户必须进入统一注册策略。双验证开启时先完成双验证,再绑定 OAuth 身份,不能通过第三方自动注册绕过。
- 邮箱域名白名单仍作用于含邮箱的新注册,不应用于既有用户登录;找回密码目的隔离,不受“关闭邮箱验证码登录”误伤。
6. 验证码与反滥用
统一 verification_challenges:用途、媒介、流程、目标摘要、验证码 HMAC、状态、过期时间、尝试次数、已用时间。验证码不明文持久化,HMAC 包含挑战 ID、用途、媒介和目标,采用用途分离的密钥派生。
建议初始值(产品配置,不是供应商限制):6 位密码学安全随机数字,5 分钟有效,60 秒重发间隔,每个挑战最多 5 次错误,每个目标每小时 5 次/每天 10 次。IP、流程、目标、渠道、全站分别限流,并给共享出口场景保留可调阈值。
- Redis 模式使用原子限流和共享冷却;单机模式也必须保证原子性。验证码唯一消费、目标唯一归属和硬性发送预算依靠数据库条件更新/事务,不能只依赖 Redis 或进程锁。
- 错误次数更新必须真实提交,不能因为返回业务错误而随验证事务回滚。耗尽后锁定挑战。
- 验证码目的包括登录、注册、绑定、密码找回,不能跨用途复用。新一代验证码发布时失效旧一代,重发受目标总额度约束。
- 全局失败预算防止攻击者通过反复申请新挑战刷新错误次数。高风险请求增加人机校验;不能只依赖浏览器倒计时。
- 登录验证码申请对账户存在性给出统一受理语义,未知账号不真实发送;系统不可用、频率超限等真实错误仍返回机器可读 reason,不伪称“短信已送达”。
- 写操作校验 Origin/CSRF 边界;验证码和手机号不得进入 URL。日志过滤 SDK 原始错误中的请求参数、密钥、正文和地址。
7. 发送、路由和状态记录
7.1 发送路径
先原子预占预算、创建挑战和发送记录,再在事务外调用供应商,最后持久化结果。第一期使用有上限的同步单次发送,验证码仅在内存中交给 SDK,不引入持久化明文验证码的通用任务队列。
记录创建失败则不发送;调用超时或调用后数据库更新失败,原记录保留为待核对/结果未知。进程崩溃遗留的发送中记录由恢复任务转为未知并对账,禁止自动再次发送。明确失败的挑战失效;结果未知但实际收到的正确验证码可在限时、限次内验证,不要求“送达回执”才能登录。
幂等请求绑定流程、场景、目标和请求摘要,同一 key 不得用来更换目标。每个逻辑消息与每次供应商尝试分开记录。
7.2 故障切换原则
| 情况 | 行为 |
|---|---|
| 首选渠道本地不可用,尚未调用供应商 | 可按配置选择下一个可用渠道 |
| 供应商明确拒绝且适配器能确认未受理 | 只有列入可切换错误白名单时才试备用渠道,并写独立尝试记录 |
| 超时、断连、响应解析失败、状态不明 | 不自动重试、不跨渠道补发,先对账或让用户冷却后明确申请新验证码 |
| 已受理但没有送达回执 | 不作为失败自动补发 |
| 参数非法、目标非法、平台风险阻断 | 直接拒绝,不通过换渠道绕过校验 |
OutId、SessionContext 等追踪字段不未经证实就当成供应商幂等保证。不宣称跨网络提供严格 exactly-once;目标是可追踪、保守重试和最小化重复计费。
7.3 两个状态维度
发送受理状态:准备中、发送中、已受理、明确失败、结果未知、平台阻断。
送达状态:等待回执、已送达、送达失败、不可确认。API 受理成功不能直接写“发送成功/已送达”。验证码消费状态独立,不与送达状态混用。
第一期优先通过经凭据认证的供应商查询接口补齐回执,带查询时间窗口、退避和停止期限;无查询能力明确显示不可确认。若某供应商使用消费式回执拉取,必须先确定独占消费者及落库策略,避免多个渠道实例争抢回执。公开 webhook 后续按供应商真实认证机制建设,不做通用无鉴权回调。
7.4 记录字段与统计
列表:时间、脱敏手机号、业务场景、渠道、模板、受理状态、送达状态、耗时、归一化错误、供应商流水号。详情展示每次渠道尝试、配置版本快照和状态时间线;支持时间、渠道、场景、状态与号码精确查询。
不存验证码、不存完整短信正文、不原样保存 SDK 请求响应。目标按需要加密保存,查询索引使用带密钥摘要,列表仅显示掩码;访问和查询均受管理员权限与审计约束。密钥只回传 hasSecret,保留/替换/清空语义明确,不能把掩码当密钥保存。
统计区分受理率和送达率,展示已查询到的计费条数;缺少实际价格时不显示伪造的准确费用。首期不提供历史验证码“一键重发”,避免重放旧码。建议日志默认保留 90 天,具体由平台配置;挑战短期保留后清理。
8. 数据模型和迁移
| 数据对象 | 关键内容 |
|---|---|
users 扩展 |
phone、phone_verified_at、email_verified_at;非空归一化手机号唯一索引 |
sms_channels |
实例 ID、插件/provider ID、启停、加密凭据、非敏感配置、配置版本、操作人 |
sms_template_bindings |
渠道、场景、模板 ID、命名变量/有序变量映射;渠道+场景唯一 |
sms_routes |
场景、首选及有序备用列表、切换策略、配置版本 |
sms_messages |
逻辑发送 ID、流程/挑战关联、幂等摘要、脱敏目标、总体状态 |
sms_send_attempts |
每次供应商调用、渠道/模板快照、请求 ID、流水号、错误及回执 |
auth_flows |
方法、用途、目标绑定、流程凭据摘要、策略版本、完成结果与过期时间 |
verification_challenges |
统一验证码挑战及尝试计数、消费状态 |
| 预算计数对象 | 按渠道/全局时间桶原子预占,未知结果保留额度,不把未确认费用退款 |
现有 SystemSetting 保存认证策略,不另造通用配置中心。数据库持久化是身份与审计真相源;Redis 仅用于协调与加速。
SQLite 与 PostgreSQL 都要覆盖迁移及并发行为。升级时关闭短信能力,按原注册总开关映射原邮箱注册策略,不自动开放注册。旧的短期验证码可明确失效并提示重取,不增加永久双写兼容层。注册及找回密码一起切换到统一挑战消费路径,避免两套不一致的限流实现。
历史邮箱验证状态迁移只采纳可证明的证据;不存在证据时保留未验证,不批量推断。上线先备份、迁移、配置并验收渠道,最后开放入口;回退先关闭新方法并使在途流程失效,不直接删除记录或回滚数据库字段。
9. 后台和前台交互
后台增加三个页面/页签:
- 短信渠道:新增、编辑、启停、场景模板、首选/备用路由、额度和最近错误;静态配置检查与实际试发分开。
- 短信记录:筛选列表、详情时间线和手动刷新回执(不是重新发送)。
- 登录与注册:方法开关、双验证互斥提示、依赖状态、注册总开关及 Linux.do 设置。
渠道编辑用现有 AppDrawer;测试发送用带明确费用提示的确认弹窗。插件管理只管理插件可用性,渠道管理负责账号实例,避免两个地方维护两套密钥。
正常停用/卸载操作检查路由与认证依赖,要求先调整依赖;安全紧急停用可以明确确认后立即阻断发送,但要同步标记受影响认证方法不可用并告警,不能悄悄把双验证降为单验证。历史记录保留渠道/provider/模板快照。
前台登录按有效能力展示密码、手机验证码、邮箱验证码入口;注册根据策略展示手机、邮箱任选或双验证表单,双验证不是两个可互相跳过的 tab。首个管理员初始化独立展示。账号安全页提供联系方式验证和绑定。
10. API 草案
以下为目标接口,不是已存在接口;与现有 auth/service/handler 统一迁移,不长期保留第二套验证码入口。
GET /api/auth/settings
POST /api/auth/flows
POST /api/auth/flows/:id/codes
POST /api/auth/login/code
POST /api/auth/register
POST /api/auth/contacts/verification
POST /api/auth/contacts/bind
GET /api/admin/sms/providers
GET /api/admin/sms/channels
POST /api/admin/sms/channels
PATCH /api/admin/sms/channels/:id
POST /api/admin/sms/channels/:id/test
GET /api/admin/sms/routes
PUT /api/admin/sms/routes/:scene
GET /api/admin/sms/messages
GET /api/admin/sms/messages/:id
POST /api/admin/sms/messages/:id/refresh-status
GET /api/admin/settings/auth-policy
PATCH /api/admin/settings/auth-policy
验证码申请只能选用途与目标,不能由未登录客户端指定供应商、渠道、模板或任意短信正文。对外仅返回流程/挑战标识、过期/重发时间和统一受理状态。测试接口限定管理员、单号码、测试模板及硬性频率上限,不创建可用于登录的挑战。
继续使用 {code,data,msg,reason} 信封。新增明确 reason,例如 AUTH_METHOD_DISABLED、REGISTRATION_DISABLED、VERIFICATION_INVALID、VERIFICATION_EXPIRED、VERIFICATION_ATTEMPTS_EXCEEDED、SMS_CHANNEL_UNAVAILABLE、SMS_QUOTA_EXCEEDED。最终名称与已有 error-code 表对齐,不以解析 msg 判断错误。
11. 实施阶段与验收
A:短信能力闭环
SDK 固定版本与适配器、系统插件贡献、渠道/模板/路由、后台试发、发送尝试和查询回执。先证明“多个渠道能安全发送并如实记录”,不提前开启公开登录入口。
B:统一验证码与认证策略
统一挑战模型、原子次数/消费、短信和邮件登录、三种注册组合、联系方式绑定、历史邮箱处理、OAuth 首次注册和初始化边界。
C:联调与发布
真实页面、供应商小流量、双数据库与多实例验收;更新数据库、HTTP API、插件开发、功能清单和 pending-test 专题。限流、配额、安全及核心失败路径属于发布前必需项,不留作上线后补丁。
必须验证:
- 同一供应商多个实例隔离;插件停用/渠道停用依赖与密钥不回显。
- 两家 SDK 请求转换、业务失败、网络超时、逐号码失败、模板参数顺序、禁用自动重试。
- 超时不自动补发;重放请求不重复发短信;发送后落库失败/进程崩溃不重复调用上游。
- 五个 UI 开关的所有合法组合、冲突拒绝、注册总开关和在途策略变化。
- 双验证任一错误/过期/已消费时不建号、不部分消费;并发提交仅一个成功。
- 验证码不可跨用途/目标/流程使用;失败计数与冷却在多实例生效。
- 既有邮箱、管理员初始化、第三方账号、手机号/邮箱冲突,不发生隐式绑定与账号合并。
- SQLite/PostgreSQL 唯一索引、事务竞争、预算硬上限和迁移默认行为。
- 后台与登录注册真实路由的明暗主题、倒计时、刷新恢复、错误/空态和键盘交互。
按风险选择专项 Go 测试、前端相关测试和构建;供应商真实验收必须有已审核签名/模板、受限测试凭据与用户认可的接收号码。本次方案阶段不运行业务测试、不启动服务、不产生短信费用。