在电商业务中,API 是核心交互通道 —— 比如订单提交、支付回调、库存查询等操作,都依赖客户端(APP / 小程序 / 网页)向服务端发送 API 请求。但如果请求参数被恶意篡改(比如把支付金额从 100 元改成 1 元),会直接造成业务损失。而API 签名验证,就是防止参数篡改的关键技术。今天咱们用 Python 手把手实现整套流程,从原理到代码一步到位。​

一、先搞懂:为什么需要 API 签名?​

举个真实风险场景:​

用户在 APP 上提交订单,需要向服务端发送order_id=123&amount=100&user_id=456这样的参数。如果没有防护,黑客可能通过抓包工具把amount=100改成amount=1,服务端若直接接收参数,就会生成 1 元的错误订单。​

API 签名的核心作用就是:​

  1. 防篡改:一旦参数被修改,签名会失效,服务端能立刻识别​
  2. 防重放:避免黑客重复提交已生效的请求(比如重复使用支付成功的请求)​
  3. 身份验证:确保请求来自合法的客户端(而非伪造的来源)​

二、签名验证的核心原理(3 步核心逻辑)​

签名验证本质是 “客户端生成签名 + 服务端验证签名” 的闭环,核心逻辑就 3 步,记牢这 3 步,后面代码就好懂了:​

1. 参数标准化(消除歧义)​

客户端和服务端必须用完全相同的规则处理参数,否则签名会不一致。关键规则:​

  • 排除空值参数(避免因空值格式差异导致签名错)​
  • 按参数名 ASCII 码升序排序(比如amount在order_id前面,因为a的 ASCII 比o小)​
  • 格式化为key=value的字符串并拼接(比如amount=100&order_id=123&user_id=456)​

2. 拼接密钥生成 “签名字符串”​

客户端和服务端共享一个唯一密钥(secret_key)(比如abc123xyz,绝对不能明文传输!),把 “标准化参数串 + 密钥” 拼接成最终的 “签名字符串”:​

amount=100&order_id=123&user_id=456abc123xyz​

3. 哈希算法生成签名​

用不可逆的哈希算法(比如 SHA256、MD5,推荐 SHA256 更安全)对 “签名字符串” 加密,得到最终的签名(比如a3f2d4e5...)。​

客户端把 “原始参数 + 签名” 一起发给服务端,服务端按同样的 3 步重新计算签名,对比客户端传来的签名 —— 如果一致,说明参数没被篡改;如果不一致,直接拒绝请求。​

三、Python 手把手实现:从客户端到服务端​

咱们分两部分实现:​

  • 客户端:构造参数→生成签名→发送请求​
  • 服务端:接收请求→验证签名→处理业务​

准备工作​

先安装需要的库(requests 用于发请求,flask 用于搭服务端):​

pip install requests flask

1. 客户端实现(生成签名 + 发送请求)​

客户端的核心是写一个 “生成签名” 的函数,再把参数和签名一起发出去。​

代码实现(client.py)​

import requests
import hashlib
import time
from urllib.parse import urlencode

def generate_sign(params, secret_key):
    """
    生成API签名的核心函数
    :param params: 原始请求参数(字典)
    :param secret_key: 客户端与服务端共享的密钥
    :return: 生成的签名(字符串)
    """
    # 步骤1:参数标准化——排除空值、按key升序排序
    # 排除空值(避免因空值导致签名不一致)
    filtered_params = {k: v for k, v in params.items() if v is not None}
    # 按参数名ASCII升序排序
    sorted_params = sorted(filtered_params.items(), key=lambda x: x[0])
    
    # 步骤2:拼接成"key=value&key=value"格式,再拼接密钥
    # 先转成url编码格式(比如处理中文、特殊字符)
    param_str = urlencode(sorted_params)
    # 拼接密钥(关键:服务端必须用同样的密钥)
    sign_str = f"{param_str}{secret_key}"
    
    # 步骤3:用SHA256哈希算法生成签名(不可逆,更安全)
    # 注意:字符串需转成bytes格式才能哈希,指定编码为utf-8(避免中文乱码)
    sign_bytes = sign_str.encode("utf-8")
    sha256 = hashlib.sha256()
    sha256.update(sign_bytes)
    # 转成16进制字符串(签名最终格式)
    sign = sha256.hexdigest()
    
    return sign

# ------------------- 实际发送请求 -------------------
if __name__ == "__main__":
    # 1. 配置基础信息
    base_url = "http://127.0.0.1:5000/api/order/submit"  # 服务端接口地址
    secret_key = "ecommerce_2024_secret_key"  # 客户端与服务端共享的密钥(线下约定,不传输)
    
    # 2. 原始请求参数(比如订单信息)
    # 注意:加timestamp(时间戳)防重放(服务端可验证时间是否在有效期内)
    params = {
        "order_id": "OD20240520001",
        "user_id": "U10086",
        "amount": 100.0,  # 支付金额(关键参数,防篡改)
        "goods_id": "G9527",
        "timestamp": int(time.time())  # 当前时间戳(秒级)
    }
    
    # 3. 生成签名
    sign = generate_sign(params, secret_key)
    # 4. 把签名加入请求参数(或放在Header里,这里简化放params)
    params["sign"] = sign
    
    # 5. 发送GET请求(实际项目也可用POST,参数处理逻辑一致)
    response = requests.get(base_url, params=params)
    print("服务端响应:", response.json())

2. 服务端实现(验证签名 + 处理业务)​

服务端的核心是 “复用客户端的签名生成逻辑”,重新计算签名并对比 —— 如果一致才处理业务,否则拒绝。​

代码实现(server.py)​

from flask import Flask, request, jsonify
import hashlib
import time
from urllib.parse import urlencode

app = Flask(__name__)

# 服务端存储的密钥(必须与客户端完全一致!)
SECRET_KEY = "ecommerce_2024_secret_key"
# 防重放:请求有效期(比如5分钟,超过则拒绝)
REQUEST_EXPIRE = 5 * 60  # 单位:秒

def generate_sign(params, secret_key):
    """
    【和客户端完全一样的签名生成函数】
    服务端必须用相同的逻辑计算签名,否则对比会失败
    """
    # 步骤1:排除空值、按key升序排序(注意:要排除客户端传来的sign字段!)
    filtered_params = {k: v for k, v in params.items() if v is not None and k != "sign"}
    sorted_params = sorted(filtered_params.items(), key=lambda x: x[0])
    
    # 步骤2:拼接参数串+密钥
    param_str = urlencode(sorted_params)
    sign_str = f"{param_str}{secret_key}"
    
    # 步骤3:SHA256哈希生成签名
    sign_bytes = sign_str.encode("utf-8")
    sha256 = hashlib.sha256()
    sha256.update(sign_bytes)
    sign = sha256.hexdigest()
    
    return sign

def verify_request(params):
    """
    验证请求合法性(签名+时间戳)
    :param params: 客户端传来的所有参数(含sign)
    :return: (是否合法, 错误信息)
    """
    # 1. 检查必要参数是否存在
    required_params = ["order_id", "user_id", "amount", "timestamp", "sign"]
    for param in required_params:
        if param not in params:
            return False, f"缺少必要参数:{param}"
    
    # 2. 验证时间戳(防重放:请求是否在有效期内)
    current_timestamp = int(time.time())
    request_timestamp = int(params["timestamp"])
    if current_timestamp - request_timestamp > REQUEST_EXPIRE:
        return False, "请求已过期(可能是重放攻击)"
    
    # 3. 验证签名
    client_sign = params["sign"]  # 客户端传来的签名
    server_sign = generate_sign(params, SECRET_KEY)  # 服务端重新计算的签名
    if client_sign != server_sign:
        return False, "签名不一致(参数可能被篡改)"
    
    # 所有验证通过
    return True, "验证通过"

# ------------------- 订单提交接口 -------------------
@app.route("/api/order/submit", methods=["GET"])
def submit_order():
    # 1. 获取客户端传来的参数(转成字典)
    params = request.args.to_dict()
    # 注意:金额等数值参数可能被转成字符串,需要手动转类型(避免签名计算时类型不一致)
    params["amount"] = float(params["amount"])
    params["timestamp"] = int(params["timestamp"])
    
    # 2. 验证请求合法性
    is_valid, err_msg = verify_request(params)
    if not is_valid:
        return jsonify({"code": 400, "msg": err_msg, "data": None})
    
    # 3. 验证通过,处理业务逻辑(比如保存订单到数据库)
    order_info = {
        "order_id": params["order_id"],
        "user_id": params["user_id"],
        "amount": params["amount"],
        "status": "pending_pay"
    }
    # 这里简化:实际项目会写数据库、调用支付接口等
    print(f"订单提交成功:{order_info}")
    
    # 4. 返回成功响应
    return jsonify({"code": 200, "msg": "订单提交成功", "data": order_info})

if __name__ == "__main__":
    # 启动服务(测试用,生产环境需用Gunicorn等)
    app.run(host="0.0.0.0", port=5000, debug=True)

四、测试:看签名验证是否生效​

咱们实际跑一下代码,验证两种场景:正常请求和参数被篡改的请求。​

1. 正常请求(参数未篡改)​

  1. 先启动服务端:运行server.py,看到* Running on http://127.0.0.1:5000说明启动成功。​
  2. 再运行客户端client.py,会看到服务端响应:​
{"code":200,"msg":"订单提交成功","data":{"order_id":"OD20240520001","user_id":"U10086","amount":100.0,"status":"pending_pay"}}

服务端控制台也会打印 “订单提交成功”,说明签名验证通过。​

2. 篡改参数测试(模拟黑客攻击)​

修改客户端params里的amount从 100.0 改成 1.0,再运行client.py:​

服务端会返回:​

{"code":400,"msg":"签名不一致(参数可能被篡改)","data":null}

这说明参数被篡改后,签名不一致,服务端成功拦截了非法请求!​

五、关键安全注意事项(必看!)​

  • 密钥绝对不能明文传输​
  • 客户端和服务端的secret_key必须线下约定(比如通过运维配置、密钥管理平台分发),绝对不能放在请求参数或 Header 里传输,否则黑客拿到密钥就能伪造签名。​
  • 敏感参数建议加密​
  • 签名只能防篡改,不能防泄露。如果参数里有手机号、身份证号等敏感信息,建议先对敏感字段单独加密(比如 AES),再参与签名计算。​
  • 配合 HTTPS 使用​
  • 签名能防篡改,但请求传输过程中参数可能被抓包查看。搭配 HTTPS(加密传输通道),能同时实现 “防篡改 + 防泄露”。​
  • 用 nonce 防重放(进阶)​
  • 除了时间戳,还可以加nonce(随机字符串,每个请求唯一),服务端记录已使用的nonce,避免黑客用 “有效期内的时间戳 + 相同 nonce” 重复提交请求。​
  • 避免用 MD5(推荐 SHA256/SHA512)​
  • MD5 哈希算法存在碰撞风险(不同内容可能生成相同 MD5),电商等核心业务建议用 SHA256 或更安全的算法。​

六、常见问题排查​

  1. 签名不一致?先查这 3 点​
  • 客户端和服务端的secret_key是否完全一致(大小写、特殊字符都要对)?​
  • 参数排序是否都是 “ASCII 升序”(比如客户端用正序,服务端用倒序就会错)?​
  • 参数类型是否一致(比如客户端传amount=100(int),服务端转成了100.0(float))?​
  1. 请求过期?检查时间戳​
  • 客户端和服务端的时间是否同步(建议都用 UTC 时间,避免时区差异)?​
  • REQUEST_EXPIRE是否设置太小(比如 1 秒,网络延迟就会导致过期)?​

总结​

API 签名验证是电商 API 安全的 “第一道防线”,核心逻辑就是 “客户端和服务端用相同规则生成签名,通过对比签名判断参数是否被篡改”。今天用 Python 实现的这套流程,能直接套用到实际项目中 —— 无论是订单、支付还是库存接口,只要按这个逻辑做签名验证,就能有效抵御参数篡改风险。​

如果你的业务有特殊需求(比如 POST 请求、JSON 参数),只需微调 “参数获取” 和 “标准化” 的逻辑,核心的签名生成和验证逻辑完全不变。动手试试吧!​

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐