9.0 KiB
9.0 KiB
短信发送模块设计
概述
基于阿里云短信服务,实现验证码发送功能,支持注册、登录、密码重置等场景。
技术栈
- 短信服务商: 阿里云短信服务
- SDK: github.com/alibabacloud-go/dysmsapi-20170525/v5
- 缓存: Redis (存储验证码)
- 频率限制: Redis (限制发送频率)
核心功能
- 验证码发送: 支持手机号验证
- 频率控制: 防止短信轰炸
- 验证码缓存: Redis存储和管理
- 错误处理: 完善的错误响应
架构设计
1. 配置结构
// internal/config/config.go
type SMSConfig struct {
Enabled bool `mapstructure:"enabled"` // 是否启用短信服务
AccessKeyID string `mapstructure:"access_key_id"` // 阿里云AccessKey ID
AccessKeySecret string `mapstructure:"access_key_secret"`// 阿里云AccessKey Secret
SignName string `mapstructure:"sign_name"` // 短信签名
RegionID string `mapstructure:"region_id"` // 地域ID,默认cn-hangzhou
Endpoint string `mapstructure:"endpoint"` // 服务端点
}
// 添加到主配置
type Config struct {
// ... 其他配置
SMS SMSConfig `mapstructure:"sms"`
}
2. 服务接口
// pkg/utils/sms.go
type SMSService interface {
SendVerificationCode(phone, phoneCountryCode, templateCode string) error
VerifyCode(phone, phoneCountryCode, code string) (bool, error)
IsRateLimited(phone, phoneCountryCode string) bool
}
type smsService struct {
client *dysmsapi20170525.Client
redis *redis.Client // 验证码缓存和频率限制
config *SMSConfig
logger *log.Logger
}
3. 配置与常量设计
为什么不完全使用配置文件?
不推荐完全配置化的原因:
- 模板代码相对稳定:阿里云模板一旦创建,不会频繁变化
- 类型安全:代码常量有编译时检查,避免运行时错误
- 重构友好:IDE可以自动重构所有引用
- 性能优化:常量在编译时确定,无运行时查找开销
推荐的混合方案
// pkg/utils/sms.go
// 默认短信模板常量(生产环境使用)
const (
DefaultTemplateRegister = "SMS_330275014"
DefaultTemplateResetPwd = "SMS_330275015"
DefaultTemplateLogin = "SMS_330275016"
)
// 缓存键常量
const (
SMSCodeKeyPrefix = "sms:code"
SMSLimitKeyPrefix = "sms:limit"
SMSCodeExpireTime = 5 * time.Minute
SMSLimitTimeWindow = time.Hour
SMSLimitMaxCount = 5
)
// SMS模板配置(支持运行时覆盖)
type SMSTemplates struct {
Register string `mapstructure:"register"`
ResetPwd string `mapstructure:"reset_pwd"`
Login string `mapstructure:"login"`
}
// 使用方法:优先使用配置,否则使用默认常量
func (s *smsService) getTemplateCode(templateType string) string {
var templateCode string
switch templateType {
case "register":
templateCode = s.templates.Register
if templateCode == "" {
templateCode = DefaultTemplateRegister
}
case "reset_pwd":
templateCode = s.templates.ResetPwd
if templateCode == "" {
templateCode = DefaultTemplateResetPwd
}
case "login":
templateCode = s.templates.Login
if templateCode == "" {
templateCode = DefaultTemplateLogin
}
}
return templateCode
}
配置文件示例
# configs/config.yaml
sms:
enabled: true
access_key_id: "your-access-key-id"
access_key_secret: "your-access-key-secret"
sign_name: "惠州顺视智能科技"
region_id: "cn-hangzhou"
endpoint: "dysmsapi.aliyuncs.com"
# 模板配置(可选,不配置则使用代码中的默认值)
templates:
register: "SMS_330275014" # 注册模板
reset_pwd: "SMS_330275015" # 重置密码模板
login: "SMS_330275016" # 登录模板
current_phone: "SMS_330275017" # 当前手机号验证模板
new_phone: "SMS_330275018" # 新手机号验证模板
注意:所有模板ID都必须在阿里云SMS控制台中存在且审核通过,否则会报错"isv.SMS_TEMPLATE_ILLEGAL"
4. 缓存设计
验证码存储
- Key:
sms:code:{country_code}:{phone} - Value: 6位数字验证码
- TTL: 5分钟
频率限制
- Key:
sms:limit:{country_code}:{phone} - Value: 发送次数计数器
- TTL: 1小时
- 限制: 每小时最多5次
5. 阿里云SMS API集成
已实现功能 ✅
- ✅ AccessKey认证方式
- ✅ 短信发送API调用
- ✅ 错误处理和响应解析
- ✅ 缓存集成(验证码存储和频率限制)
- ✅ 多环境配置支持
API调用流程
- 客户端创建: 使用AccessKeyID和AccessKeySecret创建阿里云SMS客户端
- 请求构建: 构建SendSmsRequest,包含签名、模板、手机号和参数
- API调用: 调用阿里云SMS API发送短信
- 响应处理: 解析响应,检查发送结果
- 缓存存储: 成功发送后将验证码存储到缓存
错误处理
- 认证错误: InvalidAccessKeyId.NotFound 等
- 参数错误: 手机号格式、模板不存在等
- 网络错误: 超时、重试等
- 配额错误: 发送频率限制等
测试验证
# 运行SMS服务测试
go test ./pkg/utils/ -v -run TestSMS
# 验证阿里云API调用
# 使用有效AccessKey时会成功发送短信
# 使用无效AccessKey时会返回认证错误
接口设计
发送验证码
// 发送注册验证码
func (s *smsService) SendRegisterCode(phone, phoneCountryCode string) error {
return s.SendVerificationCode(phone, phoneCountryCode, TemplateRegister)
}
// 发送重置密码验证码
func (s *smsService) SendResetPwdCode(phone, phoneCountryCode string) error {
return s.SendVerificationCode(phone, phoneCountryCode, TemplateResetPwd)
}
验证码验证
// 验证验证码
func (s *smsService) VerifyCode(phone, phoneCountryCode, code string) (bool, error) {
key := fmt.Sprintf("sms:code:%s:%s", phoneCountryCode, phone)
storedCode, err := s.redis.Get(key).Result()
if err == redis.Nil {
return false, errors.New("验证码不存在或已过期")
}
if err != nil {
return false, err
}
// 验证成功后删除验证码(一次性使用)
if storedCode == code {
s.redis.Del(key)
return true, nil
}
return false, nil
}
频率限制
// 检查频率限制
func (s *smsService) IsRateLimited(phone, phoneCountryCode string) bool {
key := fmt.Sprintf("sms:limit:%s:%s", phoneCountryCode, phone)
count, err := s.redis.Incr(key).Result()
if err != nil {
return true // Redis错误时限制发送
}
// 首次发送,设置过期时间
if count == 1 {
s.redis.Expire(key, time.Hour)
}
// 每小时最多5次
return count > 5
}
集成方式
1. Service层集成
// internal/service/cu_user_service.go
func (s *cuUserService) SendRegisterSms(ctx context.Context, req *models.CuUserSendRegisterSmsRequest) error {
// 1. 参数验证
if req == nil {
return i18n.NewI18nError("900001", lang)
}
// 2. 检查手机号是否已注册
exists, err := s.repo.CheckPhoneExists(req.Phone, req.PhoneCountryCode, "")
if err != nil {
return i18n.NewI18nError("900004", lang, err.Error())
}
if exists {
return i18n.NewI18nError("200002", lang)
}
// 3. 调用短信服务
err = s.smsService.SendRegisterCode(req.Phone, req.PhoneCountryCode)
if err != nil {
return i18n.NewI18nError("200004", lang) // 短信发送失败
}
return nil
}
2. 依赖注入
// internal/api/routes/router.go
func SetupRouter() *gin.Engine {
// ... 其他代码
// 初始化短信服务
smsService := utils.NewSMSService(&cfg.SMS, redisClient, logger)
// 初始化用户服务时注入短信服务
cuUserService := service.NewCuUserService(cuUserRepo, customerRepo, db, smsService)
// ... 其他代码
}
错误码扩展
在语言包中添加短信相关错误码:
# customer模块扩展
customer:
"200003": "请求过于频繁,请稍后再试"
"200004": "短信发送失败"
安全考虑
-
验证码保护
- 验证码一次性使用
- 过期时间控制(5分钟)
- Redis存储,防止内存泄露
-
频率控制
- 基于手机号的发送频率限制
- 防止短信轰炸攻击
- 支持国际号码
-
日志记录
- 发送成功/失败日志
- 敏感信息脱敏
- 便于问题排查
实施计划
第一阶段:基础功能
- 实现SMS服务基础类
- 集成阿里云短信SDK
- 实现验证码发送和验证
- 添加频率限制
第二阶段:完善功能
- 添加多种验证码模板
- 完善错误处理和日志
- 添加监控指标
- 性能优化
第三阶段:高级功能
- 支持国际短信
- 模板动态配置
- 批量发送功能
- 失败重试机制