This commit is contained in:
2026-02-12 13:36:24 +08:00
parent f400052f95
commit 47cbc5b3a5
2 changed files with 94 additions and 7 deletions

View File

@@ -2,9 +2,11 @@
package handlers package handlers
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strconv" "strconv"
"strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/zap" "go.uber.org/zap"
@@ -27,6 +29,7 @@ type UserHandler struct {
logger *zap.Logger logger *zap.Logger
jwtAuth *middleware.JWTAuthMiddleware jwtAuth *middleware.JWTAuthMiddleware
config *config.Config config *config.Config
cache interfaces.CacheService
} }
// NewUserHandler 创建用户处理器 // NewUserHandler 创建用户处理器
@@ -37,6 +40,7 @@ func NewUserHandler(
logger *zap.Logger, logger *zap.Logger,
jwtAuth *middleware.JWTAuthMiddleware, jwtAuth *middleware.JWTAuthMiddleware,
cfg *config.Config, cfg *config.Config,
cache interfaces.CacheService,
) *UserHandler { ) *UserHandler {
return &UserHandler{ return &UserHandler{
appService: appService, appService: appService,
@@ -45,6 +49,7 @@ func NewUserHandler(
logger: logger, logger: logger,
jwtAuth: jwtAuth, jwtAuth: jwtAuth,
config: cfg, config: cfg,
cache: cache,
} }
} }
@@ -100,15 +105,18 @@ func (h *UserHandler) SendCode(c *gin.Context) {
return return
} }
// 如果启用了签名验证,进行签名校验 // 如果启用了签名验证,进行签名校验包含nonce唯一性检查防止重放攻击
if h.config.SMS.SignatureEnabled { 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("短信发送签名验证失败", h.logger.Warn("短信发送签名验证失败",
zap.String("phone", decodedData.Phone), zap.String("phone", decodedData.Phone),
zap.String("scene", decodedData.Scene), zap.String("scene", decodedData.Scene),
zap.String("client_ip", c.ClientIP()), zap.String("client_ip", c.ClientIP()),
zap.Error(err)) zap.Error(err))
h.response.BadRequest(c, "签名验证失败,请求无效")
// 根据错误类型返回不同的用户友好消息(不暴露技术细节)
userMessage := h.getSignatureErrorMessage(err)
h.response.BadRequest(c, userMessage)
return return
} }
} }
@@ -150,8 +158,8 @@ func (h *UserHandler) decodeRequestData(encodedData string) (*decodedSendCodeDat
return &decoded, nil return &decoded, nil
} }
// verifyDecodedSignature 验证解码后的签名 // verifyDecodedSignature 验证解码后的签名包含nonce唯一性检查防止重放攻击
func (h *UserHandler) verifyDecodedSignature(data *decodedSendCodeData) error { func (h *UserHandler) verifyDecodedSignature(ctx context.Context, data *decodedSendCodeData) error {
// 构建参数map包含signature字段VerifySignature会自动排除它 // 构建参数map包含signature字段VerifySignature会自动排除它
params := map[string]string{ params := map[string]string{
"phone": data.Phone, "phone": data.Phone,
@@ -159,15 +167,40 @@ func (h *UserHandler) verifyDecodedSignature(data *decodedSendCodeData) error {
"signature": data.Signature, "signature": data.Signature,
} }
// 验证签名 // 验证签名并检查nonce唯一性防止重放攻击
return crypto.VerifySignature( return crypto.VerifySignatureWithNonceCheck(
ctx,
params, params,
h.config.SMS.SignatureSecret, h.config.SMS.SignatureSecret,
data.Timestamp, data.Timestamp,
data.Nonce, 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 用户注册 // Register 用户注册
// @Summary 用户注册 // @Summary 用户注册

View File

@@ -1,6 +1,7 @@
package crypto package crypto
import ( import (
"context"
"crypto/hmac" "crypto/hmac"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
@@ -10,6 +11,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"tyapi-server/internal/shared/interfaces"
) )
const ( const (
@@ -95,6 +98,57 @@ func VerifySignature(params map[string]string, secretKey string, timestamp int64
return nil 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字符集增加破解难度 // 自定义编码字符集不使用标准Base64字符集增加破解难度
// 使用自定义字符集:数字+大写字母排除易混淆的I和O+小写字母排除易混淆的i和l+特殊字符 // 使用自定义字符集:数字+大写字母排除易混淆的I和O+小写字母排除易混淆的i和l+特殊字符
const customEncodeCharset = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz!@#$%^&*()_+-=[]{}|;:,.<>?" const customEncodeCharset = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz!@#$%^&*()_+-=[]{}|;:,.<>?"