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
8.2 KiB
短信验证码服务实现
变更日期:2026-01-27
变更类型:新功能
影响范围:后端服务、数据库、API
变更概述
实现完整的短信验证码服务,支持登录、绑定手机、重置密码等场景。包含防刷机制、测试模式、定时清理等功能。
主要变更
1. 数据模型层
新增文件:app/models/sms.py
SmsPurposeIntEnum:用途枚举(SMALLINT 存储)LOGIN = 1:登录BIND_PHONE = 2:绑定手机RESET_PASSWORD = 3:重置密码
SmsVerificationCodeSQLModel:验证码表模型- 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.pyapp/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
使用示例
测试模式(开发环境)
- 设置
SMS_TEST_MODE=True - 调用发送接口
- 查看日志获取验证码:
[测试模式] 跳过短信发送: phone=13800138000, code=123456 - 使用日志中的验证码进行验证
生产模式
- 在阿里云短信服务控制台配置签名和模板
- 获取 AccessKey ID 和 Secret
- 设置环境变量
- 调用发送接口,用户手机收到验证码
- 用户输入验证码进行验证
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 | 短信发送失败 | 检查阿里云配置和余额 |
监控建议
关键指标
- 发送成功率:监控短信发送成功/失败比例
- 验证成功率:监控验证码验证成功/失败比例
- 限流触发次数:监控防刷机制触发频率
- 验证码过期率:监控未使用验证码的比例
日志关键字
验证码已发送:发送成功验证码验证成功:验证成功短信发送失败:发送失败发送过于频繁:触发限流
后续优化
- 多渠道支持:支持腾讯云、华为云等多个短信服务商
- 语音验证码:支持语音播报验证码
- 国际短信:支持国际手机号验证
- 验证码模板管理:支持多种验证码模板
- 发送记录查询:提供管理后台查询发送记录
相关文档
变更人:Kiro AI
审核人:待审核
变更日期:2026-01-27