172 lines
5.2 KiB
Go
172 lines
5.2 KiB
Go
|
|
// Package sms provides SMS verification code services for Tencent Cloud and Aliyun.
|
|||
|
|
package sms
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"crypto/rand"
|
|||
|
|
"errors"
|
|||
|
|
"fmt"
|
|||
|
|
"math/big"
|
|||
|
|
"time"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// ProviderType represents the SMS service provider type.
|
|||
|
|
type ProviderType string
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
ProviderTencent ProviderType = "tencent" // 腾讯云
|
|||
|
|
ProviderAliyun ProviderType = "aliyun" // 阿里云
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Config holds SMS service configuration.
|
|||
|
|
type Config struct {
|
|||
|
|
Provider ProviderType `json:"provider" yaml:"provider"`
|
|||
|
|
Enabled bool `json:"enabled" yaml:"enabled"`
|
|||
|
|
AppID string `json:"app_id" yaml:"app_id"` // 腾讯云: SDK AppID; 阿里云: AccessKey ID
|
|||
|
|
AppSecret string `json:"app_secret" yaml:"app_secret"` // 腾讯云: AppKey; 阿里云: AccessKey Secret
|
|||
|
|
SignName string `json:"sign_name" yaml:"sign_name"` // 短信签名
|
|||
|
|
TemplateCode string `json:"template_code" yaml:"template_code"` // 短信模板CODE
|
|||
|
|
Region string `json:"region" yaml:"region"` // 区域 (腾讯云用)
|
|||
|
|
Endpoint string `json:"endpoint" yaml:"endpoint"` // 自定义 endpoint (可选)
|
|||
|
|
CodeLength int `json:"code_length" yaml:"code_length"` // 验证码长度,默认6位
|
|||
|
|
CodeExpireMins int `json:"code_expire_mins" yaml:"code_expire_mins"` // 验证码有效期(分钟),默认5分钟
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// DefaultConfig returns a default SMS configuration.
|
|||
|
|
func DefaultConfig() *Config {
|
|||
|
|
return &Config{
|
|||
|
|
Enabled: false, // 默认关闭,使用硬编码验证码
|
|||
|
|
Provider: ProviderTencent,
|
|||
|
|
CodeLength: 6,
|
|||
|
|
CodeExpireMins: 5,
|
|||
|
|
Region: "ap-guangzhou",
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SMSService defines the interface for SMS verification code services.
|
|||
|
|
type SMSService interface {
|
|||
|
|
// SendVerificationCode sends an SMS verification code to the given phone number.
|
|||
|
|
// Returns the verification code ID (used for later verification) and error.
|
|||
|
|
SendVerificationCode(ctx context.Context, phoneNumber string) (string, error)
|
|||
|
|
|
|||
|
|
// VerifyCode verifies if the provided code matches the one sent to the phone.
|
|||
|
|
// Returns true if valid, false otherwise.
|
|||
|
|
VerifyCode(ctx context.Context, codeID string, phoneNumber string, code string) (bool, error)
|
|||
|
|
|
|||
|
|
// IsEnabled returns whether SMS service is enabled.
|
|||
|
|
IsEnabled() bool
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// InMemoryCodeStore stores verification codes in memory (for development/testing).
|
|||
|
|
type InMemoryCodeStore struct {
|
|||
|
|
codes map[string]*codeEntry
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type codeEntry struct {
|
|||
|
|
Code string
|
|||
|
|
Phone string
|
|||
|
|
ExpiresAt time.Time
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewInMemoryCodeStore creates a new in-memory code store.
|
|||
|
|
func NewInMemoryCodeStore() *InMemoryCodeStore {
|
|||
|
|
return &InMemoryCodeStore{
|
|||
|
|
codes: make(map[string]*codeEntry),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GenerateCode generates a random numeric code of specified length.
|
|||
|
|
func GenerateCode(length int) (string, error) {
|
|||
|
|
if length <= 0 {
|
|||
|
|
length = 6
|
|||
|
|
}
|
|||
|
|
code := ""
|
|||
|
|
for i := 0; i < length; i++ {
|
|||
|
|
n, err := rand.Int(rand.Reader, big.NewInt(10))
|
|||
|
|
if err != nil {
|
|||
|
|
return "", fmt.Errorf("failed to generate random number: %w", err)
|
|||
|
|
}
|
|||
|
|
code += fmt.Sprintf("%d", n.Int64())
|
|||
|
|
}
|
|||
|
|
return code, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MockSMSService is a mock SMS service for development/testing.
|
|||
|
|
type MockSMSService struct {
|
|||
|
|
store *InMemoryCodeStore
|
|||
|
|
config *Config
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewMockSMSService creates a new mock SMS service.
|
|||
|
|
func NewMockSMSService(config *Config) *MockSMSService {
|
|||
|
|
if config == nil {
|
|||
|
|
config = DefaultConfig()
|
|||
|
|
}
|
|||
|
|
return &MockSMSService{
|
|||
|
|
store: NewInMemoryCodeStore(),
|
|||
|
|
config: config,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// IsEnabled returns true if the SMS service is enabled.
|
|||
|
|
func (m *MockSMSService) IsEnabled() bool {
|
|||
|
|
return m.config.Enabled
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ErrSMSServiceDisabled indicates SMS service is disabled
|
|||
|
|
var ErrSMSServiceDisabled = errors.New("SMS service is disabled")
|
|||
|
|
|
|||
|
|
// SendVerificationCode generates and "sends" a verification code.
|
|||
|
|
func (m *MockSMSService) SendVerificationCode(ctx context.Context, phoneNumber string) (string, error) {
|
|||
|
|
if !m.config.Enabled {
|
|||
|
|
// 安全修复: 禁用时返回错误,而非返回mock code
|
|||
|
|
return "", ErrSMSServiceDisabled
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
code, err := GenerateCode(m.config.CodeLength)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
codeID := fmt.Sprintf("mock-%d", time.Now().UnixNano())
|
|||
|
|
expiresAt := time.Now().Add(time.Duration(m.config.CodeExpireMins) * time.Minute)
|
|||
|
|
|
|||
|
|
m.store.codes[codeID] = &codeEntry{
|
|||
|
|
Code: code,
|
|||
|
|
Phone: phoneNumber,
|
|||
|
|
ExpiresAt: expiresAt,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// In a real implementation, this would send the SMS via HTTP API
|
|||
|
|
fmt.Printf("[SMS Mock] Sending code '%s' to %s (codeID: %s)\n", code, phoneNumber, codeID)
|
|||
|
|
return codeID, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// VerifyCode verifies the provided code.
|
|||
|
|
func (m *MockSMSService) VerifyCode(ctx context.Context, codeID string, phoneNumber string, code string) (bool, error) {
|
|||
|
|
if !m.config.Enabled {
|
|||
|
|
// 安全修复: 禁用时拒绝验证,返回错误
|
|||
|
|
return false, ErrSMSServiceDisabled
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
entry, ok := m.store.codes[codeID]
|
|||
|
|
if !ok {
|
|||
|
|
return false, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if time.Now().After(entry.ExpiresAt) {
|
|||
|
|
delete(m.store.codes, codeID)
|
|||
|
|
return false, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if entry.Phone != phoneNumber {
|
|||
|
|
return false, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
valid := entry.Code == code
|
|||
|
|
if valid {
|
|||
|
|
delete(m.store.codes, codeID) // Code can only be used once
|
|||
|
|
}
|
|||
|
|
return valid, nil
|
|||
|
|
}
|