This commit is contained in:
2026-02-12 13:27:08 +08:00
parent a38c58c357
commit f400052f95
6 changed files with 770 additions and 7 deletions

View File

@@ -43,10 +43,17 @@ type ResetPasswordCommand struct {
}
// SendCodeCommand 发送验证码命令
// @Description 发送短信验证码请求参数
// @Description 发送短信验证码请求参数。只接收编码后的data字段使用自定义编码方案非Base64
type SendCodeCommand struct {
Phone string `json:"phone" binding:"required,phone" example:"13800138000"`
Scene string `json:"scene" binding:"required,oneof=register login change_password reset_password bind unbind certification" example:"register"`
// 编码后的数据使用自定义编码方案的JSON字符串包含所有参数phone, scene, timestamp, nonce, signature
Data string `json:"data" binding:"required" example:"K8mN9vP2sL7kH3oB6yC1zA5uF0qE9tW..."` // 自定义编码后的数据
// 以下字段从data解码后填充不直接接收
Phone string `json:"-"` // 从data解码后获取
Scene string `json:"-"` // 从data解码后获取
Timestamp int64 `json:"-"` // 从data解码后获取
Nonce string `json:"-"` // 从data解码后获取
Signature string `json:"-"` // 从data解码后获取
}
// UpdateProfileCommand 更新用户信息命令

View File

@@ -213,6 +213,9 @@ type SMSConfig struct {
ExpireTime time.Duration `mapstructure:"expire_time"`
RateLimit SMSRateLimit `mapstructure:"rate_limit"`
MockEnabled bool `mapstructure:"mock_enabled"` // 是否启用模拟短信服务
// 签名验证配置
SignatureEnabled bool `mapstructure:"signature_enabled"` // 是否启用签名验证
SignatureSecret string `mapstructure:"signature_secret"` // 签名密钥
}
// SMSRateLimit 短信限流配置

View File

@@ -2,6 +2,8 @@
package handlers
import (
"encoding/json"
"fmt"
"strconv"
"github.com/gin-gonic/gin"
@@ -11,6 +13,8 @@ import (
"tyapi-server/internal/application/user/dto/commands"
"tyapi-server/internal/application/user/dto/queries"
_ "tyapi-server/internal/application/user/dto/responses"
"tyapi-server/internal/config"
"tyapi-server/internal/shared/crypto"
"tyapi-server/internal/shared/interfaces"
"tyapi-server/internal/shared/middleware"
)
@@ -22,6 +26,7 @@ type UserHandler struct {
validator interfaces.RequestValidator
logger *zap.Logger
jwtAuth *middleware.JWTAuthMiddleware
config *config.Config
}
// NewUserHandler 创建用户处理器
@@ -31,6 +36,7 @@ func NewUserHandler(
validator interfaces.RequestValidator,
logger *zap.Logger,
jwtAuth *middleware.JWTAuthMiddleware,
cfg *config.Config,
) *UserHandler {
return &UserHandler{
appService: appService,
@@ -38,16 +44,26 @@ func NewUserHandler(
validator: validator,
logger: logger,
jwtAuth: jwtAuth,
config: cfg,
}
}
// decodedSendCodeData 解码后的请求数据结构
type decodedSendCodeData struct {
Phone string `json:"phone"`
Scene string `json:"scene"`
Timestamp int64 `json:"timestamp"`
Nonce string `json:"nonce"`
Signature string `json:"signature"`
}
// SendCode 发送验证码
// @Summary 发送短信验证码
// @Description 向指定手机号发送验证码,支持注册、登录、修改密码等场景
// @Description 向指定手机号发送验证码,支持注册、登录、修改密码等场景。需要提供有效的签名验证。只接收编码后的data字段使用自定义编码方案
// @Tags 用户认证
// @Accept json
// @Produce json
// @Param request body commands.SendCodeCommand true "发送验证码请求"
// @Param request body commands.SendCodeCommand true "发送验证码请求只包含data字段"
// @Success 200 {object} map[string]interface{} "验证码发送成功"
// @Failure 400 {object} map[string]interface{} "请求参数错误"
// @Failure 429 {object} map[string]interface{} "请求频率限制"
@@ -55,14 +71,61 @@ func NewUserHandler(
// @Router /api/v1/users/send-code [post]
func (h *UserHandler) SendCode(c *gin.Context) {
var cmd commands.SendCodeCommand
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
// 绑定请求只包含data字段
if err := c.ShouldBindJSON(&cmd); err != nil {
h.response.BadRequest(c, "请求参数格式错误必须提供data字段")
return
}
// 验证data字段不为空
if cmd.Data == "" {
h.response.BadRequest(c, "data字段不能为空")
return
}
// 解码自定义编码的数据
decodedData, err := h.decodeRequestData(cmd.Data)
if err != nil {
h.logger.Warn("解码请求数据失败",
zap.String("client_ip", c.ClientIP()),
zap.Error(err))
h.response.BadRequest(c, "请求数据解码失败")
return
}
// 验证必要字段
if decodedData.Phone == "" || decodedData.Scene == "" {
h.response.BadRequest(c, "手机号和场景不能为空")
return
}
// 如果启用了签名验证,进行签名校验
if h.config.SMS.SignatureEnabled {
if err := h.verifyDecodedSignature(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, "签名验证失败,请求无效")
return
}
}
// 构建SendCodeCommand用于调用应用服务
serviceCmd := &commands.SendCodeCommand{
Phone: decodedData.Phone,
Scene: decodedData.Scene,
Timestamp: decodedData.Timestamp,
Nonce: decodedData.Nonce,
Signature: decodedData.Signature,
}
clientIP := c.ClientIP()
userAgent := c.GetHeader("User-Agent")
if err := h.appService.SendCode(c.Request.Context(), &cmd, clientIP, userAgent); err != nil {
if err := h.appService.SendCode(c.Request.Context(), serviceCmd, clientIP, userAgent); err != nil {
h.response.BadRequest(c, err.Error())
return
}
@@ -70,6 +133,42 @@ func (h *UserHandler) SendCode(c *gin.Context) {
h.response.Success(c, nil, "验证码发送成功")
}
// decodeRequestData 解码自定义编码的请求数据
func (h *UserHandler) decodeRequestData(encodedData string) (*decodedSendCodeData, error) {
// 使用自定义编码方案解码
decodedData, err := crypto.DecodeRequest(encodedData)
if err != nil {
return nil, fmt.Errorf("自定义编码解码失败: %w", err)
}
// 解析JSON
var decoded decodedSendCodeData
if err := json.Unmarshal([]byte(decodedData), &decoded); err != nil {
return nil, fmt.Errorf("JSON解析失败: %w", err)
}
return &decoded, nil
}
// verifyDecodedSignature 验证解码后的签名
func (h *UserHandler) verifyDecodedSignature(data *decodedSendCodeData) error {
// 构建参数map包含signature字段VerifySignature会自动排除它
params := map[string]string{
"phone": data.Phone,
"scene": data.Scene,
"signature": data.Signature,
}
// 验证签名
return crypto.VerifySignature(
params,
h.config.SMS.SignatureSecret,
data.Timestamp,
data.Nonce,
)
}
// Register 用户注册
// @Summary 用户注册
// @Description 使用手机号、密码和验证码进行用户注册,需要确认密码

View File

@@ -0,0 +1,304 @@
package crypto
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"sort"
"strconv"
"strings"
"time"
)
const (
// SignatureTimestampTolerance 签名时间戳容差(秒),防止重放攻击
SignatureTimestampTolerance = 300 // 5分钟
)
// GenerateSignature 生成HMAC-SHA256签名
// params: 需要签名的参数map
// secretKey: 签名密钥
// timestamp: 时间戳(秒)
// nonce: 随机字符串
func GenerateSignature(params map[string]string, secretKey string, timestamp int64, nonce string) string {
// 1. 构建待签名字符串按key排序拼接成 key1=value1&key2=value2 格式
var keys []string
for k := range params {
if k != "signature" { // 排除签名字段本身
keys = append(keys, k)
}
}
sort.Strings(keys)
var parts []string
for _, k := range keys {
parts = append(parts, fmt.Sprintf("%s=%s", k, params[k]))
}
// 2. 添加时间戳和随机数
parts = append(parts, fmt.Sprintf("timestamp=%d", timestamp))
parts = append(parts, fmt.Sprintf("nonce=%s", nonce))
// 3. 拼接成待签名字符串
signString := strings.Join(parts, "&")
// 4. 使用HMAC-SHA256计算签名
mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write([]byte(signString))
signature := mac.Sum(nil)
// 5. 返回hex编码的签名
return hex.EncodeToString(signature)
}
// VerifySignature 验证HMAC-SHA256签名
// params: 请求参数map包含signature字段
// secretKey: 签名密钥
// timestamp: 时间戳(秒)
// nonce: 随机字符串
func VerifySignature(params map[string]string, secretKey string, timestamp int64, nonce string) error {
// 1. 检查签名字段是否存在
signature, exists := params["signature"]
if !exists || signature == "" {
return errors.New("签名字段缺失")
}
// 2. 验证时间戳(防止重放攻击)
now := time.Now().Unix()
if timestamp <= 0 {
return errors.New("时间戳无效")
}
if abs(now-timestamp) > SignatureTimestampTolerance {
return fmt.Errorf("请求已过期,时间戳超出容差范围(当前时间:%d请求时间%d", now, timestamp)
}
// 3. 重新计算签名
expectedSignature := GenerateSignature(params, secretKey, timestamp, nonce)
// 4. 将hex字符串转换为字节数组进行比较
signatureBytes, err := hex.DecodeString(signature)
if err != nil {
return fmt.Errorf("签名格式错误: %w", err)
}
expectedBytes, err := hex.DecodeString(expectedSignature)
if err != nil {
return fmt.Errorf("签名计算错误: %w", err)
}
// 5. 使用常量时间比较防止时序攻击
if !hmac.Equal(signatureBytes, expectedBytes) {
return errors.New("签名验证失败")
}
return nil
}
// 自定义编码字符集不使用标准Base64字符集增加破解难度
// 使用自定义字符集:数字+大写字母排除易混淆的I和O+小写字母排除易混淆的i和l+特殊字符
const customEncodeCharset = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz!@#$%^&*()_+-=[]{}|;:,.<>?"
// EncodeRequest 使用自定义编码方案编码请求参数
// 编码方式类似Base64但使用自定义字符集并加入简单的混淆
func EncodeRequest(data string) string {
// 1. 将字符串转换为字节数组
bytes := []byte(data)
// 2. 使用自定义Base64变种编码
encoded := customBase64Encode(bytes)
// 3. 添加简单的字符混淆(字符偏移)
confused := applyCharShift(encoded, 7) // 偏移7个位置
return confused
}
// DecodeRequest 解码请求参数
func DecodeRequest(encodedData string) (string, error) {
// 1. 先还原字符混淆
unconfused := reverseCharShift(encodedData, 7)
// 2. 使用自定义Base64变种解码
decoded, err := customBase64Decode(unconfused)
if err != nil {
return "", fmt.Errorf("解码失败: %w", err)
}
return string(decoded), nil
}
// customBase64Encode 自定义Base64编码使用自定义字符集
func customBase64Encode(data []byte) string {
if len(data) == 0 {
return ""
}
var result []byte
charset := []byte(customEncodeCharset)
// 将3个字节24位编码为4个字符
for i := 0; i < len(data); i += 3 {
// 读取3个字节
var b1, b2, b3 byte
b1 = data[i]
if i+1 < len(data) {
b2 = data[i+1]
}
if i+2 < len(data) {
b3 = data[i+2]
}
// 组合成24位
combined := uint32(b1)<<16 | uint32(b2)<<8 | uint32(b3)
// 分成4个6位段
result = append(result, charset[(combined>>18)&0x3F])
result = append(result, charset[(combined>>12)&0x3F])
if i+1 < len(data) {
result = append(result, charset[(combined>>6)&0x3F])
} else {
result = append(result, '=') // 填充字符
}
if i+2 < len(data) {
result = append(result, charset[combined&0x3F])
} else {
result = append(result, '=') // 填充字符
}
}
return string(result)
}
// customBase64Decode 自定义Base64解码
func customBase64Decode(encoded string) ([]byte, error) {
if len(encoded) == 0 {
return []byte{}, nil
}
charset := []byte(customEncodeCharset)
charsetMap := make(map[byte]int)
for i, c := range charset {
charsetMap[c] = i
}
var result []byte
data := []byte(encoded)
// 将4个字符解码为3个字节
for i := 0; i < len(data); i += 4 {
if i+3 >= len(data) {
return nil, fmt.Errorf("编码数据长度不正确")
}
// 获取4个字符的索引
var idx [4]int
for j := 0; j < 4; j++ {
if data[i+j] == '=' {
idx[j] = 0 // 填充字符
} else {
val, ok := charsetMap[data[i+j]]
if !ok {
return nil, fmt.Errorf("无效的编码字符: %c", data[i+j])
}
idx[j] = val
}
}
// 组合成24位
combined := uint32(idx[0])<<18 | uint32(idx[1])<<12 | uint32(idx[2])<<6 | uint32(idx[3])
// 提取3个字节
result = append(result, byte((combined>>16)&0xFF))
if data[i+2] != '=' {
result = append(result, byte((combined>>8)&0xFF))
}
if data[i+3] != '=' {
result = append(result, byte(combined&0xFF))
}
}
return result, nil
}
// applyCharShift 应用字符偏移混淆
func applyCharShift(data string, shift int) string {
charset := customEncodeCharset
charsetLen := len(charset)
result := make([]byte, len(data))
for i, c := range []byte(data) {
if c == '=' {
result[i] = c // 填充字符不变
continue
}
// 查找字符在字符集中的位置
idx := -1
for j, ch := range []byte(charset) {
if ch == c {
idx = j
break
}
}
if idx == -1 {
result[i] = c // 不在字符集中,保持不变
} else {
// 应用偏移
newIdx := (idx + shift) % charsetLen
result[i] = charset[newIdx]
}
}
return string(result)
}
// reverseCharShift 还原字符偏移混淆
func reverseCharShift(data string, shift int) string {
charset := customEncodeCharset
charsetLen := len(charset)
result := make([]byte, len(data))
for i, c := range []byte(data) {
if c == '=' {
result[i] = c // 填充字符不变
continue
}
// 查找字符在字符集中的位置
idx := -1
for j, ch := range []byte(charset) {
if ch == c {
idx = j
break
}
}
if idx == -1 {
result[i] = c // 不在字符集中,保持不变
} else {
// 还原偏移
newIdx := (idx - shift + charsetLen) % charsetLen
result[i] = charset[newIdx]
}
}
return string(result)
}
// abs 计算绝对值
func abs(x int64) int64 {
if x < 0 {
return -x
}
return x
}
// ParseTimestamp 从字符串解析时间戳
func ParseTimestamp(ts string) (int64, error) {
return strconv.ParseInt(ts, 10, 64)
}