You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

8.2 KiB

短信验证码服务实现

变更日期:2026-01-27
变更类型:新功能
影响范围:后端服务、数据库、API


变更概述

实现完整的短信验证码服务,支持登录、绑定手机、重置密码等场景。包含防刷机制、测试模式、定时清理等功能。


主要变更

1. 数据模型层

新增文件app/models/sms.py

  • SmsPurpose IntEnum:用途枚举(SMALLINT 存储)
    • LOGIN = 1:登录
    • BIND_PHONE = 2:绑定手机
    • RESET_PASSWORD = 3:重置密码
  • SmsVerificationCode SQLModel:验证码表模型
    • UUID v7 主键
    • 支持多种用途
    • 包含过期时间、验证状态、IP 地址等字段

数据库表结构

CREATE TABLE sms_verification_codes (
    id UUID PRIMARY KEY DEFAULT gen_uuid_v7(),
    phone VARCHAR(20) NOT NULL,
    country_code VARCHAR(10) NOT NULL DEFAULT '+86',
    code VARCHAR(10) NOT NULL,
    purpose SMALLINT NOT NULL DEFAULT 1,
    expires_at TIMESTAMPTZ NOT NULL,
    verified BOOLEAN DEFAULT false,
    ip_address INET,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    deleted_at TIMESTAMPTZ
);

索引优化

  • 单列索引:phone, purpose, ip_address
  • 组合索引:(phone, country_code, purpose, verified, expires_at)
  • 条件索引:expires_at WHERE verified = false AND deleted_at IS NULL

2. 数据访问层

新增文件app/repositories/sms_repository.py

核心方法:

  • create() - 创建验证码
  • get_valid_code() - 查询有效验证码
  • mark_as_verified() - 标记已验证
  • delete_expired() - 软删除过期记录

3. 业务逻辑层

新增文件app/services/sms_service.py

核心功能:

3.1 发送验证码

  • 生成 6 位随机数字验证码
  • 防刷检查(Redis 限流)
  • 调用阿里云短信 API(支持测试模式)
  • 存储验证码到数据库
  • 有效期 10 分钟

3.2 验证验证码

  • 查询未验证且未过期的验证码
  • 验证码匹配检查
  • 验证成功后立即失效

3.3 防刷机制

  • IP 限流:1分钟内最多 3 次
  • 手机号限流:1分钟内最多 1 次,1小时内最多 5 次
  • 使用 Redis 实现分布式限流

3.4 测试模式

  • 环境变量 SMS_TEST_MODE=True 启用测试模式
  • 测试模式跳过真实短信发送,仅打印日志
  • 生产模式调用阿里云 API

4. API 层

新增文件app/schemas/sms.py

Schema 定义:

  • SmsPurposeEnum:API 层用途枚举(字符串)
  • SmsCodeSendRequest:发送验证码请求
  • SmsCodeVerifyRequest:验证验证码请求
  • SmsCodeSendResponse:发送验证码响应
  • SmsCodeVerifyResponse:验证验证码响应

更新文件app/api/v1/auth.py

新增接口:

4.1 发送短信验证码

POST /api/v1/auth/sms/send

请求体:

{
  "phone": "13800138000",
  "countryCode": "+86",
  "purpose": "login"
}

响应:

{
  "code": 200,
  "message": "Success",
  "data": {
    "message": "验证码已发送",
    "expiresIn": 600
  }
}

4.2 验证短信验证码

POST /api/v1/auth/sms/verify

请求体:

{
  "phone": "13800138000",
  "countryCode": "+86",
  "code": "123456",
  "purpose": "login"
}

响应:

{
  "code": 200,
  "message": "Success",
  "data": {
    "success": true
  }
}

5. 定时任务

新增文件app/tasks/sms_tasks.py

  • clean_expired_sms_codes():清理过期验证码
  • 执行频率:每小时
  • 保留策略:保留最近 24 小时的记录用于审计

6. 配置更新

更新文件app/core/config.py

新增配置项:

SMS_TEST_MODE: bool = True  # 测试模式开关
SMS_ACCESS_KEY_ID: str = ""
SMS_ACCESS_KEY_SECRET: str = ""
SMS_SIGN_NAME: str = "道研组"
SMS_TEMPLATE_CODE: str = ""

更新文件.env.example

# 短信服务配置(阿里云)
SMS_TEST_MODE=True
SMS_ACCESS_KEY_ID=
SMS_ACCESS_KEY_SECRET=
SMS_SIGN_NAME=道研组
SMS_TEMPLATE_CODE=

7. 数据库迁移

新增文件

  • app/migrations/011_sms_service_tables.py
  • app/migrations/sql/011_sms_service_tables.sql

迁移内容:

  • 创建 sms_verification_codes
  • 创建相关索引
  • 添加表和列注释

技术亮点

1. 全异步实现

  • 使用 asyncpg 异步数据库访问
  • 使用 httpx.AsyncClient 异步 HTTP 请求
  • 使用 redis.asyncio 异步 Redis 操作

2. 枚举类型优化

  • 数据库层使用 SMALLINT 存储(节省空间)
  • API 层使用字符串枚举(易读性)
  • 提供双向转换方法

3. 防刷机制

  • 多维度限流(IP + 手机号)
  • 分布式限流(Redis)
  • 灵活的时间窗口控制

4. 测试友好

  • 支持测试模式,避免开发阶段产生短信费用
  • 测试模式下打印验证码到日志
  • 环境变量控制,无需修改代码

5. 安全性

  • 验证码有效期 10 分钟
  • 验证成功后立即失效
  • 记录请求 IP 地址
  • 软删除保留审计记录

部署步骤

1. 更新环境变量

编辑 .env 文件:

# 测试环境
SMS_TEST_MODE=True

# 生产环境
SMS_TEST_MODE=False
SMS_ACCESS_KEY_ID=your_access_key_id
SMS_ACCESS_KEY_SECRET=your_access_key_secret
SMS_SIGN_NAME=道研组
SMS_TEMPLATE_CODE=SMS_123456789

2. 执行数据库迁移

cd server
python app/migrations/011_sms_service_tables.py

3. 配置 Celery 定时任务

在 Celery Beat 配置中添加:

from celery.schedules import crontab

beat_schedule = {
    'clean-expired-sms-codes': {
        'task': 'sms.clean_expired_codes',
        'schedule': crontab(minute=0),  # 每小时执行
    },
}

4. 重启服务

# 重启 API 服务
docker-compose restart api

# 重启 Celery Worker
docker-compose restart celery-worker

# 重启 Celery Beat
docker-compose restart celery-beat

使用示例

测试模式(开发环境)

  1. 设置 SMS_TEST_MODE=True
  2. 调用发送接口
  3. 查看日志获取验证码:
    [测试模式] 跳过短信发送: phone=13800138000, code=123456
    
  4. 使用日志中的验证码进行验证

生产模式

  1. 在阿里云短信服务控制台配置签名和模板
  2. 获取 AccessKey ID 和 Secret
  3. 设置环境变量
  4. 调用发送接口,用户手机收到验证码
  5. 用户输入验证码进行验证

API 测试

发送验证码

curl -X POST http://localhost:6170/api/v1/auth/sms/send \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "13800138000",
    "countryCode": "+86",
    "purpose": "login"
  }'

验证验证码

curl -X POST http://localhost:6170/api/v1/auth/sms/verify \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "13800138000",
    "countryCode": "+86",
    "code": "123456",
    "purpose": "login"
  }'

错误处理

常见错误码

错误码 说明 解决方案
429 发送过于频繁 等待限流时间窗口过期
400 验证码不存在或已过期 重新发送验证码
400 验证码错误 检查输入的验证码
500 短信发送失败 检查阿里云配置和余额

监控建议

关键指标

  1. 发送成功率:监控短信发送成功/失败比例
  2. 验证成功率:监控验证码验证成功/失败比例
  3. 限流触发次数:监控防刷机制触发频率
  4. 验证码过期率:监控未使用验证码的比例

日志关键字

  • 验证码已发送:发送成功
  • 验证码验证成功:验证成功
  • 短信发送失败:发送失败
  • 发送过于频繁:触发限流

后续优化

  1. 多渠道支持:支持腾讯云、华为云等多个短信服务商
  2. 语音验证码:支持语音播报验证码
  3. 国际短信:支持国际手机号验证
  4. 验证码模板管理:支持多种验证码模板
  5. 发送记录查询:提供管理后台查询发送记录

相关文档


变更人:Kiro AI
审核人:待审核
变更日期:2026-01-27