262 lines
6.2 KiB
Go
262 lines
6.2 KiB
Go
package services
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// SMSCodeInfo 短信验证码信息
|
|
type SMSCodeInfo struct {
|
|
Code string // 验证码
|
|
Phone string // 手机号
|
|
IP string // IP地址
|
|
CreatedAt time.Time // 创建时间
|
|
ExpiresAt time.Time // 过期时间
|
|
}
|
|
|
|
// RateLimitInfo 频率限制信息
|
|
type RateLimitInfo struct {
|
|
Count int // 请求次数
|
|
FirstTime time.Time // 首次请求时间
|
|
LastTime time.Time // 最后请求时间
|
|
}
|
|
|
|
// SMSService 短信验证码服务
|
|
type SMSService struct {
|
|
// 验证码存储:key为手机号,value为验证码信息
|
|
codes sync.Map // map[string]*SMSCodeInfo
|
|
|
|
// 手机号频率限制:key为手机号,value为限制信息
|
|
phoneLimits sync.Map // map[string]*RateLimitInfo
|
|
|
|
// IP频率限制:key为IP地址,value为限制信息
|
|
ipLimits sync.Map // map[string]*RateLimitInfo
|
|
}
|
|
|
|
var (
|
|
smsServiceInstance *SMSService
|
|
smsServiceOnce sync.Once
|
|
)
|
|
|
|
// GetSMSService 获取短信服务单例
|
|
func GetSMSService() *SMSService {
|
|
smsServiceOnce.Do(func() {
|
|
smsServiceInstance = &SMSService{}
|
|
// 启动清理协程,定期清理过期数据
|
|
go smsServiceInstance.cleanup()
|
|
})
|
|
return smsServiceInstance
|
|
}
|
|
|
|
// GenerateCode 生成6位数字验证码
|
|
func (s *SMSService) GenerateCode() string {
|
|
// 使用时间戳作为随机种子,确保每次生成的验证码都不同
|
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
return fmt.Sprintf("%06d", r.Intn(1000000))
|
|
}
|
|
|
|
// SendSMS 发送短信验证码
|
|
// phone: 手机号
|
|
// ip: 客户端IP地址
|
|
// 返回:验证码、错误信息
|
|
func (s *SMSService) SendSMS(phone, ip string) (string, error) {
|
|
// 1. 验证手机号格式
|
|
if !s.validatePhone(phone) {
|
|
return "", fmt.Errorf("手机号格式不正确")
|
|
}
|
|
|
|
// 2. 检查手机号频率限制(同一手机号1分钟内只能发送1次)
|
|
if err := s.checkPhoneLimit(phone); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// 3. 检查IP频率限制(同一IP1分钟内最多发送5次)
|
|
if err := s.checkIPLimit(ip); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// 4. 生成验证码
|
|
code := s.GenerateCode()
|
|
|
|
// 5. 存储验证码信息(有效期5分钟)
|
|
codeInfo := &SMSCodeInfo{
|
|
Code: code,
|
|
Phone: phone,
|
|
IP: ip,
|
|
CreatedAt: time.Now(),
|
|
ExpiresAt: time.Now().Add(5 * time.Minute),
|
|
}
|
|
s.codes.Store(phone, codeInfo)
|
|
|
|
// 6. 更新频率限制记录
|
|
s.updatePhoneLimit(phone)
|
|
s.updateIPLimit(ip)
|
|
|
|
// TODO: 这里应该调用真实的短信服务API发送验证码
|
|
// 目前只返回验证码,实际生产环境需要调用短信服务商API
|
|
fmt.Printf("[SMS] 发送验证码到 %s: %s (IP: %s)\n", phone, code, ip)
|
|
|
|
return code, nil
|
|
}
|
|
|
|
// VerifyCode 验证验证码
|
|
func (s *SMSService) VerifyCode(phone, code string) bool {
|
|
value, ok := s.codes.Load(phone)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
codeInfo := value.(*SMSCodeInfo)
|
|
|
|
// 检查是否过期
|
|
if time.Now().After(codeInfo.ExpiresAt) {
|
|
s.codes.Delete(phone)
|
|
return false
|
|
}
|
|
|
|
// 验证码匹配
|
|
if codeInfo.Code == code {
|
|
// 验证成功后删除验证码(一次性使用)
|
|
s.codes.Delete(phone)
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// validatePhone 验证手机号格式
|
|
func (s *SMSService) validatePhone(phone string) bool {
|
|
if len(phone) != 11 {
|
|
return false
|
|
}
|
|
// 简单验证:1开头,第二位3-9
|
|
if phone[0] != '1' || phone[1] < '3' || phone[1] > '9' {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// checkPhoneLimit 检查手机号频率限制
|
|
// 同一手机号1分钟内只能发送1次
|
|
func (s *SMSService) checkPhoneLimit(phone string) error {
|
|
value, ok := s.phoneLimits.Load(phone)
|
|
if !ok {
|
|
return nil // 没有限制记录,可以发送
|
|
}
|
|
|
|
limitInfo := value.(*RateLimitInfo)
|
|
// 检查是否在限制时间内(1分钟)
|
|
if time.Since(limitInfo.LastTime) < 1*time.Minute {
|
|
return fmt.Errorf("操作过于频繁,请1分钟后再试")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkIPLimit 检查IP频率限制
|
|
// 同一IP1分钟内最多发送5次
|
|
func (s *SMSService) checkIPLimit(ip string) error {
|
|
value, ok := s.ipLimits.Load(ip)
|
|
if !ok {
|
|
return nil // 没有限制记录,可以发送
|
|
}
|
|
|
|
limitInfo := value.(*RateLimitInfo)
|
|
|
|
// 如果超过1分钟,重置计数
|
|
if time.Since(limitInfo.FirstTime) >= 1*time.Minute {
|
|
limitInfo.Count = 0
|
|
limitInfo.FirstTime = time.Now()
|
|
return nil
|
|
}
|
|
|
|
// 检查是否超过限制(1分钟内最多5次)
|
|
if limitInfo.Count >= 5 {
|
|
return fmt.Errorf("请求过于频繁,请稍后再试")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// updatePhoneLimit 更新手机号限制记录
|
|
func (s *SMSService) updatePhoneLimit(phone string) {
|
|
value, ok := s.phoneLimits.Load(phone)
|
|
if !ok {
|
|
// 创建新记录
|
|
s.phoneLimits.Store(phone, &RateLimitInfo{
|
|
Count: 1,
|
|
FirstTime: time.Now(),
|
|
LastTime: time.Now(),
|
|
})
|
|
return
|
|
}
|
|
|
|
limitInfo := value.(*RateLimitInfo)
|
|
limitInfo.Count++
|
|
limitInfo.LastTime = time.Now()
|
|
}
|
|
|
|
// updateIPLimit 更新IP限制记录
|
|
func (s *SMSService) updateIPLimit(ip string) {
|
|
value, ok := s.ipLimits.Load(ip)
|
|
if !ok {
|
|
// 创建新记录
|
|
s.ipLimits.Store(ip, &RateLimitInfo{
|
|
Count: 1,
|
|
FirstTime: time.Now(),
|
|
LastTime: time.Now(),
|
|
})
|
|
return
|
|
}
|
|
|
|
limitInfo := value.(*RateLimitInfo)
|
|
|
|
// 如果超过1分钟,重置计数
|
|
if time.Since(limitInfo.FirstTime) >= 1*time.Minute {
|
|
limitInfo.Count = 1
|
|
limitInfo.FirstTime = time.Now()
|
|
limitInfo.LastTime = time.Now()
|
|
} else {
|
|
limitInfo.Count++
|
|
limitInfo.LastTime = time.Now()
|
|
}
|
|
}
|
|
|
|
// cleanup 定期清理过期数据
|
|
func (s *SMSService) cleanup() {
|
|
ticker := time.NewTicker(2 * time.Minute) // 每10分钟清理一次
|
|
defer ticker.Stop()
|
|
|
|
for range ticker.C {
|
|
now := time.Now()
|
|
|
|
// 清理过期的验证码
|
|
s.codes.Range(func(key, value interface{}) bool {
|
|
codeInfo := value.(*SMSCodeInfo)
|
|
if now.After(codeInfo.ExpiresAt) {
|
|
s.codes.Delete(key)
|
|
}
|
|
return true
|
|
})
|
|
|
|
// 清理过期的频率限制记录(超过1小时未使用的记录)
|
|
s.phoneLimits.Range(func(key, value interface{}) bool {
|
|
limitInfo := value.(*RateLimitInfo)
|
|
if now.Sub(limitInfo.LastTime) > 1*time.Hour {
|
|
s.phoneLimits.Delete(key)
|
|
}
|
|
return true
|
|
})
|
|
|
|
s.ipLimits.Range(func(key, value interface{}) bool {
|
|
limitInfo := value.(*RateLimitInfo)
|
|
if now.Sub(limitInfo.LastTime) > 1*time.Hour {
|
|
s.ipLimits.Delete(key)
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
}
|