f
This commit is contained in:
@@ -129,6 +129,9 @@ sms:
|
||||
code_length: 6
|
||||
expire_time: 5m
|
||||
mock_enabled: false
|
||||
# 签名验证配置(用于防止接口被刷)
|
||||
signature_enabled: true # 是否启用签名验证
|
||||
signature_secret: "TyApi2024SMSSecretKey!@#$%^&*()_+QWERTYUIOP" # 签名密钥(请修改为复杂密钥)
|
||||
rate_limit:
|
||||
daily_limit: 10
|
||||
hourly_limit: 5
|
||||
|
||||
347
docs/短信接口签名验证使用指南.md
Normal file
347
docs/短信接口签名验证使用指南.md
Normal file
@@ -0,0 +1,347 @@
|
||||
# 短信接口签名验证使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
为了防止短信发送接口被恶意刷取,系统实现了基于HMAC-SHA256的签名验证机制。所有发送短信的请求必须包含有效的签名,否则请求将被拒绝。
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. **前端生成签名**:使用密钥对请求参数进行HMAC-SHA256签名
|
||||
2. **后端验证签名**:后端使用相同密钥重新计算签名并比对
|
||||
3. **时间戳验证**:防止重放攻击,时间戳必须在5分钟内有效
|
||||
4. **随机数验证**:每次请求必须包含唯一的随机字符串(nonce)
|
||||
5. **参数编码传输**(推荐):将所有参数(包括签名)编码成Base64字符串后传输,隐藏参数结构,增加安全性
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 后端配置
|
||||
|
||||
在 `config.yaml` 中配置签名相关参数:
|
||||
|
||||
```yaml
|
||||
sms:
|
||||
# ... 其他配置 ...
|
||||
# 签名验证配置
|
||||
signature_enabled: true # 是否启用签名验证
|
||||
signature_secret: "TyApi2024SMSSecretKey!@#$%^&*()_+QWERTYUIOP" # 签名密钥(请修改为复杂密钥)
|
||||
```
|
||||
|
||||
**重要提示**:
|
||||
- 生产环境必须修改 `signature_secret` 为复杂的随机字符串
|
||||
- 密钥长度建议至少32个字符
|
||||
- 密钥应包含大小写字母、数字和特殊字符
|
||||
|
||||
## 签名算法
|
||||
|
||||
### 1. 构建待签名字符串
|
||||
|
||||
将请求参数(排除`signature`字段)按key排序,拼接成以下格式:
|
||||
|
||||
```
|
||||
key1=value1&key2=value2×tamp=1234567890&nonce=random_string
|
||||
```
|
||||
|
||||
### 2. 计算HMAC-SHA256签名
|
||||
|
||||
使用配置的密钥对待签名字符串进行HMAC-SHA256计算,结果转换为hex编码。
|
||||
|
||||
### 3. 请求参数
|
||||
|
||||
系统支持两种请求方式:
|
||||
|
||||
#### 方式1:直接传递参数
|
||||
|
||||
发送请求时直接传递所有字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"phone": "13800138000",
|
||||
"scene": "register",
|
||||
"timestamp": 1704067200,
|
||||
"nonce": "a1b2c3d4e5f6g7h8",
|
||||
"signature": "abc123def456..."
|
||||
}
|
||||
```
|
||||
|
||||
#### 方式2:编码后传输(推荐,更安全)
|
||||
|
||||
将所有参数(包括签名)编码成Base64字符串后传输,只传递一个`data`字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": "eyJwaG9uZSI6IjEzODAwMTM4MDAwIiwic2NlbmUiOiJyZWdpc3RlciIsInRpbWVzdGFtcCI6MTcwNDA2NzIwMCwibm9uY2UiOiJhMWIyYzNkNGE1ZjYiLCJzaWduYXR1cmUiOiJhYmMxMjNkZWY0NTYifQ=="
|
||||
}
|
||||
```
|
||||
|
||||
**编码传输的优势**:
|
||||
- 隐藏参数结构,增加破解难度
|
||||
- 参数不可见,防止参数被直接修改
|
||||
- 增加一层编码保护
|
||||
|
||||
## 前端实现
|
||||
|
||||
### Node.js 示例
|
||||
|
||||
参考文件:`tyapi-frontend/public/examples/nodejs/sms_signature_demo.js`
|
||||
|
||||
#### 方式1:直接传递参数
|
||||
|
||||
```javascript
|
||||
const crypto = require('crypto');
|
||||
|
||||
function generateSignature(params, secretKey, timestamp, nonce) {
|
||||
// 1. 构建待签名字符串
|
||||
const keys = Object.keys(params)
|
||||
.filter(k => k !== 'signature')
|
||||
.sort();
|
||||
|
||||
const parts = keys.map(k => `${k}=${params[k]}`);
|
||||
parts.push(`timestamp=${timestamp}`);
|
||||
parts.push(`nonce=${nonce}`);
|
||||
|
||||
const signString = parts.join('&');
|
||||
|
||||
// 2. 计算HMAC-SHA256签名
|
||||
const signature = crypto
|
||||
.createHmac('sha256', secretKey)
|
||||
.update(signString)
|
||||
.digest('hex');
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const params = { phone: '13800138000', scene: 'register' };
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const nonce = generateRandomString(16);
|
||||
const secretKey = 'your_secret_key';
|
||||
const signature = generateSignature(params, secretKey, timestamp, nonce);
|
||||
|
||||
// 发送请求
|
||||
const requestBody = {
|
||||
phone: '13800138000',
|
||||
scene: 'register',
|
||||
timestamp: timestamp,
|
||||
nonce: nonce,
|
||||
signature: signature,
|
||||
};
|
||||
```
|
||||
|
||||
#### 方式2:编码后传输(推荐)
|
||||
|
||||
```javascript
|
||||
// 1. 生成签名(同上)
|
||||
const params = { phone: '13800138000', scene: 'register' };
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const nonce = generateRandomString(16);
|
||||
const secretKey = 'your_secret_key';
|
||||
const signature = generateSignature(params, secretKey, timestamp, nonce);
|
||||
|
||||
// 2. 构建包含所有参数的JSON对象
|
||||
const allParams = {
|
||||
phone: '13800138000',
|
||||
scene: 'register',
|
||||
timestamp: timestamp,
|
||||
nonce: nonce,
|
||||
signature: signature,
|
||||
};
|
||||
|
||||
// 3. 编码为Base64
|
||||
const jsonString = JSON.stringify(allParams);
|
||||
const encodedData = Buffer.from(jsonString).toString('base64');
|
||||
|
||||
// 4. 发送请求(只传递data字段)
|
||||
const requestBody = {
|
||||
data: encodedData,
|
||||
};
|
||||
```
|
||||
|
||||
### 浏览器 JavaScript 示例
|
||||
|
||||
参考文件:`tyapi-frontend/public/examples/javascript/sms_signature_demo.js`
|
||||
|
||||
#### 方式1:直接传递参数
|
||||
|
||||
```javascript
|
||||
async function generateSignature(params, secretKey, timestamp, nonce) {
|
||||
// 1. 构建待签名字符串
|
||||
const keys = Object.keys(params)
|
||||
.filter(k => k !== 'signature')
|
||||
.sort();
|
||||
|
||||
const parts = keys.map(k => `${k}=${params[k]}`);
|
||||
parts.push(`timestamp=${timestamp}`);
|
||||
parts.push(`nonce=${nonce}`);
|
||||
|
||||
const signString = parts.join('&');
|
||||
|
||||
// 2. 使用Web Crypto API计算HMAC-SHA256签名
|
||||
const encoder = new TextEncoder();
|
||||
const keyData = encoder.encode(secretKey);
|
||||
const messageData = encoder.encode(signString);
|
||||
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
keyData,
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
);
|
||||
|
||||
const signature = await crypto.subtle.sign('HMAC', cryptoKey, messageData);
|
||||
const hashArray = Array.from(new Uint8Array(signature));
|
||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
return hashHex;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const params = { phone: '13800138000', scene: 'register' };
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const nonce = generateNonce(16);
|
||||
const secretKey = 'your_secret_key';
|
||||
const signature = await generateSignature(params, secretKey, timestamp, nonce);
|
||||
|
||||
// 发送请求
|
||||
const requestBody = {
|
||||
phone: '13800138000',
|
||||
scene: 'register',
|
||||
timestamp: timestamp,
|
||||
nonce: nonce,
|
||||
signature: signature,
|
||||
};
|
||||
```
|
||||
|
||||
#### 方式2:编码后传输(推荐)
|
||||
|
||||
```javascript
|
||||
// 1. 生成签名(同上)
|
||||
const params = { phone: '13800138000', scene: 'register' };
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const nonce = generateNonce(16);
|
||||
const secretKey = 'your_secret_key';
|
||||
const signature = await generateSignature(params, secretKey, timestamp, nonce);
|
||||
|
||||
// 2. 构建包含所有参数的JSON对象
|
||||
const allParams = {
|
||||
phone: '13800138000',
|
||||
scene: 'register',
|
||||
timestamp: timestamp,
|
||||
nonce: nonce,
|
||||
signature: signature,
|
||||
};
|
||||
|
||||
// 3. 编码为Base64(浏览器环境)
|
||||
const jsonString = JSON.stringify(allParams);
|
||||
const encodedData = btoa(unescape(encodeURIComponent(jsonString)));
|
||||
|
||||
// 4. 发送请求(只传递data字段)
|
||||
const requestBody = {
|
||||
data: encodedData,
|
||||
};
|
||||
```
|
||||
|
||||
## 密钥隐藏策略
|
||||
|
||||
由于前端代码可以被查看,完全隐藏密钥是不可能的。但可以通过以下方式增加破解难度:
|
||||
|
||||
### 1. 字符串拆分和拼接
|
||||
|
||||
```javascript
|
||||
function getSecretKey() {
|
||||
const part1 = 'TyApi2024';
|
||||
const part2 = 'SMSSecret';
|
||||
const part3 = 'Key!@#$%^';
|
||||
return part1 + part2 + part3;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 字符数组拼接
|
||||
|
||||
```javascript
|
||||
function getSecretKey() {
|
||||
const chars = ['T', 'y', 'A', 'p', 'i', ...];
|
||||
return chars.join('');
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Base64编码混淆
|
||||
|
||||
```javascript
|
||||
function getSecretKey() {
|
||||
const encoded = 'base64_encoded_string';
|
||||
return atob(encoded);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 代码混淆
|
||||
|
||||
使用构建工具(如webpack、rollup等)进行代码混淆和压缩,使密钥更难被发现。
|
||||
|
||||
### 5. 后端代理(推荐)
|
||||
|
||||
将签名逻辑放在后端代理接口中,前端只调用代理接口,不直接包含密钥。
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **定期更换密钥**:建议每3-6个月更换一次签名密钥
|
||||
2. **监控异常请求**:监控签名验证失败的请求,及时发现攻击行为
|
||||
3. **结合其他防护措施**:
|
||||
- IP限流
|
||||
- 设备指纹识别
|
||||
- 验证码(图形验证码)
|
||||
- 行为分析
|
||||
|
||||
4. **日志记录**:记录所有签名验证失败的请求,包括IP、User-Agent等信息
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见错误
|
||||
|
||||
1. **签名字段缺失**:返回 `"签名字段缺失"`
|
||||
2. **时间戳无效**:返回 `"时间戳无效"`
|
||||
3. **请求已过期**:返回 `"请求已过期,时间戳超出容差范围"`
|
||||
4. **签名验证失败**:返回 `"签名验证失败"`
|
||||
|
||||
### 时间戳容差
|
||||
|
||||
系统允许的时间戳容差为 **5分钟**(300秒)。如果请求时间戳与服务器时间差超过5分钟,请求将被拒绝。
|
||||
|
||||
## 测试
|
||||
|
||||
### 测试签名生成
|
||||
|
||||
```bash
|
||||
# 使用Node.js示例
|
||||
node tyapi-frontend/public/examples/nodejs/sms_signature_demo.js
|
||||
```
|
||||
|
||||
### 测试API调用
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/v1/users/send-code \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"phone": "13800138000",
|
||||
"scene": "register",
|
||||
"timestamp": 1704067200,
|
||||
"nonce": "a1b2c3d4e5f6g7h8",
|
||||
"signature": "计算得到的签名"
|
||||
}'
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **时间同步**:确保客户端和服务器时间同步,避免时间戳验证失败
|
||||
2. **随机数唯一性**:每次请求的nonce应该是唯一的,可以使用UUID或时间戳+随机数
|
||||
3. **密钥安全**:生产环境密钥不要提交到代码仓库,应使用环境变量或密钥管理服务
|
||||
4. **向后兼容**:如果需要在开发环境禁用签名验证,可以设置 `signature_enabled: false`
|
||||
|
||||
## 相关文件
|
||||
|
||||
- 后端签名工具:`internal/shared/crypto/signature.go`
|
||||
- 后端Handler:`internal/infrastructure/http/handlers/user_handler.go`
|
||||
- 配置结构:`internal/config/config.go`
|
||||
- Node.js示例:`tyapi-frontend/public/examples/nodejs/sms_signature_demo.js`
|
||||
- 浏览器示例:`tyapi-frontend/public/examples/javascript/sms_signature_demo.js`
|
||||
|
||||
@@ -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 更新用户信息命令
|
||||
|
||||
@@ -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 短信限流配置
|
||||
|
||||
@@ -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 使用手机号、密码和验证码进行用户注册,需要确认密码
|
||||
|
||||
304
internal/shared/crypto/signature.go
Normal file
304
internal/shared/crypto/signature.go
Normal 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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user