Add the untracked SMS service package and its security-focused tests, plus database-backed IAM service unit coverage. This keeps the verification surface small and brings previously validated test/runtime support sources into version control as an isolated change.
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
|
||
}
|