接口签名验证防止参数篡改

wen java案例 2

本文目录导读:

接口签名验证防止参数篡改

  1. 签名验证核心流程
  2. 标准实现步骤
  3. 关键安全措施
  4. 不同语言的实现示例
  5. 安全性增强建议
  6. 完整验证流程示例
  7. 注意事项

接口签名验证是防止参数篡改的核心手段,其原理是对请求参数进行哈希运算生成签名,服务端用相同算法校验,以下是详细实现方案和最佳实践:

签名验证核心流程

客户端:参数 + 密钥 → 排序 → 拼接 → 哈希 → 签名
服务端:接收参数 + 存储密钥 → 相同算法 → 比对签名

标准实现步骤

客户端签名生成

import hashlib
import time
import random
from urllib.parse import urlencode
def generate_sign(params, secret_key):
    """
    生成请求签名
    :param params: dict 请求参数字典
    :param secret_key: str 客户端密钥
    :return: dict 包含签名和随机参数的完整请求数据
    """
    # 1. 添加防重放攻击参数
    params['timestamp'] = int(time.time())  # 时间戳
    params['nonce'] = str(random.randint(100000, 999999))  # 随机数
    # 2. 参数排序(按key的ASCII升序)
    sorted_params = sorted(params.items(), key=lambda x: x[0])
    # 3. 拼接成字符串 key1=value1&key2=value2
    sign_string = urlencode(sorted_params)
    # 4. 加入密钥,生成签名
    sign_string += f'&key={secret_key}'
    # 5. 使用MD5(或SHA256)生成签名
    signature = hashlib.md5(sign_string.encode()).hexdigest()
    # 6. 添加签名到请求参数
    params['sign'] = signature
    return params

服务端签名验证

def verify_sign(params, secret_key, timeout=300):
    """
    验证请求签名
    :param params: dict 请求参数(包含sign)
    :param secret_key: str 服务端存储的密钥
    :param timeout: int 时间戳允许的最大偏差(秒)
    :return: bool
    """
    # 1. 检查必要参数
    required = ['sign', 'timestamp', 'nonce']
    if not all(k in params for k in required):
        return False
    # 2. 时间戳防重放
    current_time = int(time.time())
    if abs(current_time - int(params['timestamp'])) > timeout:
        return False
    # 3. 随机数防重复(需结合缓存)
    # if nonce_exists(params['nonce']): return False
    # 4. 提取签名并移除
    client_sign = params.pop('sign')
    # 5. 按相同规则生成签名
    sorted_params = sorted(params.items(), key=lambda x: x[0])
    sign_string = urlencode(sorted_params)
    sign_string += f'&key={secret_key}'
    server_sign = hashlib.md5(sign_string.encode()).hexdigest()
    # 6. 对比签名(使用安全比较防止时序攻击)
    return hmac.compare_digest(server_sign, client_sign)

关键安全措施

防重放攻击

# 时间戳校验(示例中已实现)
# 随机数缓存(防止同一nonce重复使用)
import redis
r = redis.Redis()
def check_nonce(nonce, timestamp):
    """检查随机数是否已使用"""
    key = f"nonce:{nonce}"
    if r.exists(key):
        return False
    # 设置过期时间(与时间戳偏差一致)
    r.setex(key, 300, timestamp)
    return True

多密钥支持

def verify_sign_with_appid(params):
    """支持多个客户端(不同密钥)"""
    app_id = params.get('app_id')
    secret_key = get_secret_key_by_app_id(app_id)  # 从数据库获取
    if not secret_key:
        return False
    return verify_sign(params, secret_key)

不同语言的实现示例

JavaScript(前端/Node.js)

const crypto = require('crypto');
function generateSign(params, secretKey) {
    // 添加时间戳和随机数
    params.timestamp = Math.floor(Date.now() / 1000);
    params.nonce = Math.random().toString(36).substring(2, 15);
    // 排序并拼接
    const sortedKeys = Object.keys(params).sort();
    let signStr = sortedKeys.map(key => `${key}=${params[key]}`).join('&');
    signStr += `&key=${secretKey}`;
    // MD5签名
    return crypto.createHash('md5').update(signStr).digest('hex');
}

Java

import java.security.MessageDigest;
import java.util.*;
public class SignUtil {
    public static String generateSign(Map<String, String> params, String secretKey) {
        // 添加时间戳和随机数
        params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
        params.put("nonce", UUID.randomUUID().toString().replace("-", ""));
        // 排序
        TreeMap<String, String> sortedMap = new TreeMap<>(params);
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> entry : sortedMap.entrySet()) {
            sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
        }
        sb.append("key=").append(secretKey);
        // MD5
        return DigestUtils.md5DigestAsHex(sb.toString().getBytes());
    }
}

安全性增强建议

使用HMAC代替简单拼接

import hmac
# 替换 签名拼接方式
signature = hmac.new(secret_key.encode(), sign_string.encode(), hashlib.sha256).hexdigest()

参数过滤

# 排除空值和sign参数本身
def filter_params(params):
    return {k: v for k, v in params.items() 
            if k != 'sign' and v is not None and v != ''}

URL编码处理

# 防止特殊字符导致的签名错误
from urllib.parse import quote
param_value = quote(str(value), safe='')

完整验证流程示例

from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/api/data', methods=['POST'])
def api_handler():
    # 1. 获取请求参数
    params = request.get_json() or request.form.to_dict()
    # 2. 获取客户端密钥(根据app_id)
    app_id = params.get('app_id')
    secret_key = get_client_secret(app_id)
    # 3. 验证签名
    if not verify_sign(params, secret_key):
        return jsonify({'code': 401, 'message': '签名验证失败'}), 401
    # 4. 处理业务逻辑
    return jsonify({'code': 200, 'message': '成功'})
if __name__ == '__main__':
    app.run()

注意事项

  1. 密钥管理:客户端密钥不能硬编码在代码中,应通过安全渠道分发
  2. HTTPS优先:签名验证需要配合HTTPS使用,防止中间人截获密钥
  3. 性能考虑:对于高并发场景,可使用异步缓存(Redis)存储nonce
  4. 错误处理:签名失败时不应泄露具体是哪个参数错误
  5. 日志记录:记录签名验证失败的请求信息,用于安全审计

通过以上方案,可以有效防止参数在传输过程中被篡改,保障API接口的安全性。

抱歉,评论功能暂时关闭!