From 47cbc5b3a5ed9f6b1cdf444c06f4d60e03b7a74f Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Thu, 12 Feb 2026 13:36:24 +0800 Subject: [PATCH] f --- .../http/handlers/user_handler.go | 47 +++++++++++++--- internal/shared/crypto/signature.go | 54 +++++++++++++++++++ 2 files changed, 94 insertions(+), 7 deletions(-) diff --git a/internal/infrastructure/http/handlers/user_handler.go b/internal/infrastructure/http/handlers/user_handler.go index 27f9460..7697768 100644 --- a/internal/infrastructure/http/handlers/user_handler.go +++ b/internal/infrastructure/http/handlers/user_handler.go @@ -2,9 +2,11 @@ package handlers import ( + "context" "encoding/json" "fmt" "strconv" + "strings" "github.com/gin-gonic/gin" "go.uber.org/zap" @@ -27,6 +29,7 @@ type UserHandler struct { logger *zap.Logger jwtAuth *middleware.JWTAuthMiddleware config *config.Config + cache interfaces.CacheService } // NewUserHandler 创建用户处理器 @@ -37,6 +40,7 @@ func NewUserHandler( logger *zap.Logger, jwtAuth *middleware.JWTAuthMiddleware, cfg *config.Config, + cache interfaces.CacheService, ) *UserHandler { return &UserHandler{ appService: appService, @@ -45,6 +49,7 @@ func NewUserHandler( logger: logger, jwtAuth: jwtAuth, config: cfg, + cache: cache, } } @@ -100,15 +105,18 @@ func (h *UserHandler) SendCode(c *gin.Context) { return } - // 如果启用了签名验证,进行签名校验 + // 如果启用了签名验证,进行签名校验(包含nonce唯一性检查,防止重放攻击) if h.config.SMS.SignatureEnabled { - if err := h.verifyDecodedSignature(decodedData); err != nil { + if err := h.verifyDecodedSignature(c.Request.Context(), decodedData); err != nil { h.logger.Warn("短信发送签名验证失败", zap.String("phone", decodedData.Phone), zap.String("scene", decodedData.Scene), zap.String("client_ip", c.ClientIP()), zap.Error(err)) - h.response.BadRequest(c, "签名验证失败,请求无效") + + // 根据错误类型返回不同的用户友好消息(不暴露技术细节) + userMessage := h.getSignatureErrorMessage(err) + h.response.BadRequest(c, userMessage) return } } @@ -150,8 +158,8 @@ func (h *UserHandler) decodeRequestData(encodedData string) (*decodedSendCodeDat return &decoded, nil } -// verifyDecodedSignature 验证解码后的签名 -func (h *UserHandler) verifyDecodedSignature(data *decodedSendCodeData) error { +// verifyDecodedSignature 验证解码后的签名(包含nonce唯一性检查,防止重放攻击) +func (h *UserHandler) verifyDecodedSignature(ctx context.Context, data *decodedSendCodeData) error { // 构建参数map(包含signature字段,VerifySignature会自动排除它) params := map[string]string{ "phone": data.Phone, @@ -159,15 +167,40 @@ func (h *UserHandler) verifyDecodedSignature(data *decodedSendCodeData) error { "signature": data.Signature, } - // 验证签名 - return crypto.VerifySignature( + // 验证签名并检查nonce唯一性(防止重放攻击) + return crypto.VerifySignatureWithNonceCheck( + ctx, params, h.config.SMS.SignatureSecret, data.Timestamp, data.Nonce, + h.cache, + "sms:signature", // 缓存键前缀 ) } +// getSignatureErrorMessage 根据错误类型返回用户友好的错误消息(不暴露技术细节) +func (h *UserHandler) getSignatureErrorMessage(err error) string { + errMsg := err.Error() + + // 根据错误消息内容判断错误类型,返回通用的用户友好消息 + if strings.Contains(errMsg, "请求已被使用") || strings.Contains(errMsg, "重复提交") { + // 重放攻击:返回通用消息,不暴露具体原因 + return "请求无效,请重新操作" + } + if strings.Contains(errMsg, "时间戳") || strings.Contains(errMsg, "过期") { + // 时间戳过期:返回通用消息 + return "请求已过期,请重新操作" + } + if strings.Contains(errMsg, "签名") { + // 签名错误:返回通用消息 + return "请求验证失败,请重新操作" + } + + // 其他错误:返回通用消息 + return "请求验证失败,请重新操作" +} + // Register 用户注册 // @Summary 用户注册 diff --git a/internal/shared/crypto/signature.go b/internal/shared/crypto/signature.go index 2f031ec..a98ad4b 100644 --- a/internal/shared/crypto/signature.go +++ b/internal/shared/crypto/signature.go @@ -1,6 +1,7 @@ package crypto import ( + "context" "crypto/hmac" "crypto/sha256" "encoding/hex" @@ -10,6 +11,8 @@ import ( "strconv" "strings" "time" + + "tyapi-server/internal/shared/interfaces" ) const ( @@ -95,6 +98,57 @@ func VerifySignature(params map[string]string, secretKey string, timestamp int64 return nil } +// VerifySignatureWithNonceCheck 验证HMAC-SHA256签名并检查nonce唯一性(防止重放攻击) +// params: 请求参数map(包含signature字段) +// secretKey: 签名密钥 +// timestamp: 时间戳(秒) +// nonce: 随机字符串 +// cache: 缓存服务,用于存储已使用的nonce +// cacheKeyPrefix: 缓存键前缀 +func VerifySignatureWithNonceCheck( + ctx context.Context, + params map[string]string, + secretKey string, + timestamp int64, + nonce string, + cache interfaces.CacheService, + cacheKeyPrefix string, +) error { + // 1. 先进行基础签名验证 + if err := VerifySignature(params, secretKey, timestamp, nonce); err != nil { + return err + } + + // 2. 检查nonce是否已被使用(防止重放攻击) + // 使用请求指纹:phone+timestamp+nonce 作为唯一标识 + phone := params["phone"] + if phone == "" { + return errors.New("手机号不能为空") + } + + // 构建nonce唯一性检查的缓存键 + nonceKey := fmt.Sprintf("%s:nonce:%s:%d:%s", cacheKeyPrefix, phone, timestamp, nonce) + + // 检查nonce是否已被使用 + exists, err := cache.Exists(ctx, nonceKey) + if err != nil { + // 缓存查询失败,记录错误但继续验证(避免缓存故障导致服务不可用) + return fmt.Errorf("检查nonce唯一性失败: %w", err) + } + if exists { + return errors.New("请求已被使用,请勿重复提交") + } + + // 3. 将nonce标记为已使用,TTL设置为时间戳容差+1分钟(确保在容差范围内不会重复使用) + ttl := time.Duration(SignatureTimestampTolerance+60) * time.Second + if err := cache.Set(ctx, nonceKey, true, ttl); err != nil { + // 记录错误但不影响验证流程(避免缓存故障导致服务不可用) + return fmt.Errorf("标记nonce已使用失败: %w", err) + } + + return nil +} + // 自定义编码字符集(不使用标准Base64字符集,增加破解难度) // 使用自定义字符集:数字+大写字母(排除易混淆的I和O)+小写字母(排除易混淆的i和l)+特殊字符 const customEncodeCharset = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz!@#$%^&*()_+-=[]{}|;:,.<>?"