Files
tyapi-server/docs/短信接口签名验证使用指南.md
2026-02-12 13:27:08 +08:00

9.5 KiB
Raw Blame History

短信接口签名验证使用指南

概述

为了防止短信发送接口被恶意刷取系统实现了基于HMAC-SHA256的签名验证机制。所有发送短信的请求必须包含有效的签名否则请求将被拒绝。

工作原理

  1. 前端生成签名使用密钥对请求参数进行HMAC-SHA256签名
  2. 后端验证签名:后端使用相同密钥重新计算签名并比对
  3. 时间戳验证防止重放攻击时间戳必须在5分钟内有效
  4. 随机数验证每次请求必须包含唯一的随机字符串nonce
  5. 参数编码传输推荐将所有参数包括签名编码成Base64字符串后传输隐藏参数结构增加安全性

配置说明

后端配置

config.yaml 中配置签名相关参数:

sms:
    # ... 其他配置 ...
    # 签名验证配置
    signature_enabled: true  # 是否启用签名验证
    signature_secret: "TyApi2024SMSSecretKey!@#$%^&*()_+QWERTYUIOP"  # 签名密钥(请修改为复杂密钥)

重要提示

  • 生产环境必须修改 signature_secret 为复杂的随机字符串
  • 密钥长度建议至少32个字符
  • 密钥应包含大小写字母、数字和特殊字符

签名算法

1. 构建待签名字符串

将请求参数(排除signature字段按key排序拼接成以下格式

key1=value1&key2=value2&timestamp=1234567890&nonce=random_string

2. 计算HMAC-SHA256签名

使用配置的密钥对待签名字符串进行HMAC-SHA256计算结果转换为hex编码。

3. 请求参数

系统支持两种请求方式:

方式1直接传递参数

发送请求时直接传递所有字段:

{
    "phone": "13800138000",
    "scene": "register",
    "timestamp": 1704067200,
    "nonce": "a1b2c3d4e5f6g7h8",
    "signature": "abc123def456..."
}

方式2编码后传输推荐更安全

将所有参数包括签名编码成Base64字符串后传输只传递一个data字段:

{
    "data": "eyJwaG9uZSI6IjEzODAwMTM4MDAwIiwic2NlbmUiOiJyZWdpc3RlciIsInRpbWVzdGFtcCI6MTcwNDA2NzIwMCwibm9uY2UiOiJhMWIyYzNkNGE1ZjYiLCJzaWduYXR1cmUiOiJhYmMxMjNkZWY0NTYifQ=="
}

编码传输的优势

  • 隐藏参数结构,增加破解难度
  • 参数不可见,防止参数被直接修改
  • 增加一层编码保护

前端实现

Node.js 示例

参考文件:tyapi-frontend/public/examples/nodejs/sms_signature_demo.js

方式1直接传递参数

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编码后传输推荐

// 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直接传递参数

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编码后传输推荐

// 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. 字符串拆分和拼接

function getSecretKey() {
    const part1 = 'TyApi2024';
    const part2 = 'SMSSecret';
    const part3 = 'Key!@#$%^';
    return part1 + part2 + part3;
}

2. 字符数组拼接

function getSecretKey() {
    const chars = ['T', 'y', 'A', 'p', 'i', ...];
    return chars.join('');
}

3. Base64编码混淆

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分钟请求将被拒绝。

测试

测试签名生成

# 使用Node.js示例
node tyapi-frontend/public/examples/nodejs/sms_signature_demo.js

测试API调用

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
  • 后端Handlerinternal/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