3. API v3 接口规范:接口签名机制、请求头构造、幂等性与重ty策略

好,咱们进入正题。API v3 是微信支付目前主推的接口规范。说实话,我刚接触 v3 的时候,也觉得签名挺绕的。但用顺手了你会发现,它比 v2 安全得多,也规范得多。

这一节,我带你手撕 v3 的核心机制。包括签名怎么算、请求头怎么拼、幂等性怎么用、重试怎么搞。嗯,都是实战中每天要面对的东西。

3.1 接口签名机制:你发的请求,微信怎么信你?

签名,说白了就是给请求加个「防伪标签」。微信收到你的请求后,会用同样的算法算一遍签名。如果对不上,直接拒掉。

我个人习惯把签名过程拆成三步:

  1. 拼字符串:把请求方法、URL、时间戳、随机数、请求体拼成一行。
  2. 加签:用你的商户私钥,对字符串做 SHA256-RSA2048 签名。
  3. 塞头:把签名结果放进 HTTP 头里,发给微信。

来看一个具体的例子。假设你要查一笔订单:

GET /v3/pay/transactions/id/4200001234202411110000000001
Host: api.mch.weixin.qq.com
Authorization: WECHATPAY2-SHA256-RSA2048
  mchid="1230000109",
  nonce_str="d5e6f7a8b9c0",
  timestamp="1731234567",
  serial_no="45C8A5B9F6D2E7A1B3C4D5E6F7A8B9C0D1E2F3A4",
  signature="base64_encoded_signature"

注意看 Authorization 头。它里面包含了商户号、随机串、时间戳、证书序列号,还有最终的签名值。

核心要点:签名用的私钥,是你申请商户证书时下载的那个。千万别弄丢了。我见过有人把私钥放代码仓库里,结果被扫出来,直接导致资金损失。

3.2 请求头构造:少了哪个头,请求都过不去

v3 接口对请求头要求很严格。少了任何一个,微信都会返回 400 Bad Request。我刚开始对接时,就吃过这个亏。

必须带的请求头有这几个:

请求头 说明 示例值
Authorization 签名信息,格式固定 WECHATPAY2-SHA256-RSA2048 ...
Content-Type 请求体格式 application/json
User-Agent 客户端标识 Mozilla/5.0 ...
Accept 响应格式 application/json

其中 Authorization 的构造最复杂。我一般会写一个工具函数,专门生成这个头。代码大概长这样:

def build_authorization(method, url, body, mch_id, serial_no, private_key):
    # 1. 生成随机串和时间戳
    nonce = generate_nonce()
    timestamp = str(int(time.time()))
    
    # 2. 拼待签名字符串
    message = f"{method}\n{url}\n{timestamp}\n{nonce}\n{body}\n"
    
    # 3. 用私钥签名
    signature = rsa_sign(message, private_key)
    
    # 4. 组装 Authorization 头
    auth = (
        f'WECHATPAY2-SHA256-RSA2048 '
        f'mchid="{mch_id}",'
        f'nonce_str="{nonce}",'
        f'timestamp="{timestamp}",'
        f'serial_no="{serial_no}",'
        f'signature="{signature}"'
    )
    return auth

我的经验:随机串一定要够随机。我曾经用时间戳当随机串,结果被安全团队通报了。建议用 uuid4 或者 secrets.token_hex 生成。

3.3 幂等性:同一个请求,发两次会怎样?

幂等性,说白了就是「同一个请求,发一次和发一百次,效果一样」。微信支付 v3 用 Idempotency-Key 头来实现。

为什么会需要这个?你想想看,网络是不稳定的。你发起一个支付请求,微信处理成功了,但响应在网络中丢了。这时候你该不该重试?

如果不重试,用户可能付了钱但订单没更新。如果重试,用户可能被扣两次钱。幂等性就是来解决这个问题的。

使用方式很简单:

  1. 每次请求生成一个唯一的 Idempotency-Key(比如用订单号 + 随机数)。
  2. 把这个 key 放在请求头里。
  3. 微信收到请求后,如果发现同样的 key 已经处理过,就直接返回上次的结果。

注意:幂等性只对写操作有效(比如下单、退款)。查询接口本身是幂等的,不需要加这个头。另外,幂等 key 的有效期一般是 24 小时。过期后,同样的 key 会被当作新请求处理。

3.4 重试策略:失败了怎么办?

重试是支付系统的必修课。但重试不是无脑重试。我见过有人遇到失败就立刻重试,结果把微信服务器打挂了。

合理的重试策略应该是这样的:

  • 指数退避:第一次失败等 1 秒,第二次等 2 秒,第三次等 4 秒,以此类推。
  • 最大重试次数:一般 3-5 次就够了。超过这个次数,说明系统有问题,需要人工介入。
  • 区分错误类型:如果是 4xx(比如签名错误),重试也没用。如果是 5xx 或网络超时,可以重试。

我曾经在一个项目中,把重试逻辑写成了死循环。结果线上跑了半小时,发了上万次重复请求。嗯,那天的值班电话被打爆了。

下面是一个推荐的重试代码模板:

def request_with_retry(method, url, body, max_retries=3):
    for attempt in range(max_retries):
        try:
            response = send_request(method, url, body)
            if response.status_code < 500:
                return response
        except (TimeoutError, ConnectionError) as e:
            print(f"第 {attempt+1} 次重试,错误:{e}")
        
        # 指数退避
        time.sleep(2 ** attempt)
    
    raise Exception("重试次数耗尽,请求失败")

避坑指南:我曾经遇到过一个坑——重试时忘了重新生成 Idempotency-Key。结果第二次请求用了同样的 key,微信直接返回了第一次的结果。但第一次的结果其实是失败的。嗯,从那以后,我每次重试都会生成新的 key。

3.5 实战总结:一个完整的请求流程

好了,我们把上面所有知识点串起来。一个完整的 v3 请求流程是这样的:

  1. 生成 Idempotency-Key(写操作需要)。
  2. 构造请求体(JSON 格式)。
  3. 生成 Authorization 头(签名)。
  4. 设置其他请求头(Content-Type、User-Agent 等)。
  5. 发送请求。
  6. 如果失败,判断是否可重试。如果可以,按指数退避重试。
  7. 如果成功,解析响应。注意验证响应的签名(微信也会对响应签名)。

我个人习惯把上面这些逻辑封装成一个 SDK 类。这样业务代码只需要调用 client.get()client.post(),不用关心签名和重试的细节。

嗯,这一节的内容就到这里。下一节我们会讲如何验证微信回调通知的签名。那个也很重要,因为回调里直接带着支付结果。