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