Files
lijiaoqiao/supply-api/internal/sms/sms.go
Your Name 193372ca95 test(supply-api): add sms module coverage and iam db service tests
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.
2026-04-11 10:28:26 +08:00

172 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}