f
This commit is contained in:
@@ -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 用户注册
|
||||
|
||||
@@ -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!@#$%^&*()_+-=[]{}|;:,.<>?"
|
||||
|
||||
Reference in New Issue
Block a user