PAYATHON 2026

如何在 Django 网站中集成银行支付系统并解决 SSL 错误

支付小周

结论

这类支付一般分为两个阶段:

  1. Django 后端调用银行支付接口,获取交易 ID、会话令牌或支付页面地址。
  2. 浏览器跳转到银行托管的支付页面,客户直接输入银行卡信息。支付完成后,银行通过返回地址、后台通知或查询接口反馈交易结果。

当前错误出现在第一阶段的 TLS 握手过程中,请求还没有到达银行的支付业务接口。Python 无法验证银行服务器返回的证书链,原因可能是银行使用了私有 CA、服务端没有发送完整的中间证书,或者该接口要求商户提供双向 TLS 客户端证书。

生产环境中不要用 verify=False 绕过验证。应先向银行确认各个证书文件的用途,再正确配置 CA 证书或客户端证书。

完整支付流程

问题中没有提供银行文档,因此具体参数名和响应格式必须以银行文档为准。常见的支付流程如下。

1. Django 后端创建订单

先在自己的数据库中创建一笔待支付订单,至少保存:

  • 唯一订单号
  • 支付金额和币种
  • 当前状态,例如 pending
  • 创建时间
  • 用户或业务对象
  • 银行返回的交易 ID

订单号必须唯一。发起支付时,不要直接使用浏览器提交的金额,而要从数据库中重新读取,防止参数被篡改。

2. 后端向银行注册交易

Django 后端通过 HTTPS 向银行提交金额、币种、订单号、客户 IP 和返回地址等参数。

参数可以通过 data 或 json 传递,不要手工拼接查询字符串。接口使用表单还是 JSON,应以银行的要求为准。

import requests

BANK_PAYMENT_URL = (
    "https://ecomm.pashabank.az:18443/ecomm2/MerchantHandler/"
)

def register_payment(order, client_ip):
    payload = {
        "command": "v",
        "amount": str(order.amount),
        "currency": order.currency_code,
        "client_ip_addr": client_ip,
        "msg_type": "SMS",
    }

    response = requests.post(
        BANK_PAYMENT_URL,
        data=payload,
        timeout=(5, 30),
        verify="/secure/path/bank-ca-chain.pem",
    )
    response.raise_for_status()
    return response.text

amount 应使用整数最小货币单位还是带小数的金额,currency 应填写哪种代码,都要严格按照银行文档处理,不能自行猜测。

3. 解析银行响应

银行可能返回:

  • 交易 ID
  • 支付会话令牌
  • 重定向 URL
  • 错误码及错误说明

Django 需要同时检查 HTTP 状态码和银行业务错误码,再将交易 ID 与本地订单绑定并保存。HTTP 状态码为 200,并不一定表示交易注册成功。

原代码中的:

print(response)

通常只会输出类似 <Response [200]> 的内容。排查问题时还应查看:

print(response.status_code)
print(response.headers)
print(response.text)

生产日志不要记录密码、令牌、银行卡信息或完整的敏感响应。

4. 将客户送到银行支付页面

后端的 requests.post() 不会打开新窗口。这个请求运行在服务器上,无法直接操作客户的浏览器。

通常可以让 Django 返回重定向:

from django.shortcuts import redirect

def start_payment(request, order_id):
    order = get_order_for_payment(order_id)
    payment_url = register_and_get_payment_url(order, request)
    return redirect(payment_url)

如果业务确实要求打开新窗口,应由前端在用户点击按钮时调用 window.open()。如果等异步请求完成后再打开窗口,浏览器可能将其当作弹窗拦截,所以直接跳转通常更可靠。

银行卡号、有效期、CVV 和 3-D Secure 验证信息应直接填写在银行托管的页面中,不应经过 Django 服务器。只有银行明确提供其他经过合规认证的接入模式时,才应采用其他方式。

5. 接收支付结果并主动核验

支付完成后,银行通常会通过两种方式通知商户:

  • 浏览器返回地址:银行将客户带回商户网站。
  • 后台通知地址:银行服务器直接向 Django 发送通知。

不能只根据浏览器返回结果认定支付成功,因为用户可能关闭窗口、修改请求参数或重复访问返回地址。收到通知后,应验证银行签名。如果银行提供查询接口,还应主动核对交易状态、金额、币种和订单号。

处理通知时还需要满足以下要求:

  • 同一笔交易的通知即使多次到达,也只能入账一次。
  • 金额、币种和订单号必须与本地订单一致。
  • 只有银行确认交易成功后,才能将订单状态改为 paid。
  • 保存银行原始交易 ID,以便后续退款和对账。
  • 不要因为用户没有返回商户网站就认定支付失败。

SSL 错误为什么会出现

报错中的关键信息是:

certificate verify failed: self signed certificate in certificate chain

建立 TLS 连接时,银行服务器会发送自己的证书和证书链。requests 会检查:

  • 证书是否由可信 CA 签发
  • 证书链是否完整
  • 证书是否仍在有效期内
  • 证书中的主机名是否包含 ecomm.pashabank.az
  • 证书是否允许用于服务器身份认证

当前环境无法将银行返回的证书链连接到本机信任的根证书,因此验证失败。常见原因包括:

  1. 银行使用私有 CA 或自签名 CA,需要单独信任银行提供的 CA 证书。
  2. 银行服务器没有发送完整的中间证书链。
  3. 银行提供的是测试环境证书,而系统默认信任库并不信任该证书。
  4. 网络中的代理、防火墙或安全设备替换了 HTTPS 证书。
  5. 当前配置使用了错误、过期或不匹配的证书文件。

Max retries exceeded 只是 requests 最外层的异常描述。真正的原因仍是 TLS 证书验证失败,单纯增加重试次数无法解决。

银行提供的 certificates 应该怎么用

首先要向银行确认每个证书文件的用途。常见文件分为三类,不能混用。

CA 根证书或中间证书

这类证书用于验证银行服务器。将银行指定的根证书和中间证书整理为 PEM 证书链,再通过 verify 指定:

response = requests.post(
    BANK_PAYMENT_URL,
    data=payload,
    verify="/secure/path/bank-ca-chain.pem",
    timeout=(5, 30),
)

verify 应指向可信 CA 文件,而不是随意选择一张服务器证书。银行最好能提供正确的 CA bundle 和证书指纹,商户还应通过独立的可信渠道进行确认。

客户端证书和私钥

如果银行要求双向 TLS(mTLS),商户除了验证银行服务器,还需要向银行证明自己的身份:

response = requests.post(
    BANK_PAYMENT_URL,
    data=payload,
    verify="/secure/path/bank-ca-chain.pem",
    cert=(
        "/secure/path/merchant-client-cert.pem",
        "/secure/path/merchant-client-key.pem",
    ),
    timeout=(5, 30),
)

客户端证书必须与私钥配对。私钥文件应设置严格的访问权限,不能提交到 Git,也不能放在可由 Web 服务器直接下载的静态目录中。

如果银行提供的是 .p12 或 .pfx 文件,其中通常包含客户端证书和私钥。requests 不能将它当作普通 PEM 文件直接使用,一般需要在安全环境中转换:

openssl pkcs12 -in merchant.p12 -clcerts -nokeys -out merchant-client-cert.pem
openssl pkcs12 -in merchant.p12 -nocerts -out merchant-client-key.pem

第二条命令默认生成受密码保护的私钥。是否需要将其转换为无密码私钥,取决于部署方式和使用的 TLS 库。如果部署环境确实要求无密码私钥,必须同时配置操作系统权限、密钥管理系统和访问审计,不能只为使用方便而随意移除保护。

签名证书或公钥

有些证书不参与 TLS 连接,而是用于验证支付通知的数字签名。这类证书应在处理回调时使用:

银行通知数据 + 银行签名
             ↓
使用银行公钥验证签名
             ↓
核对订单号、金额、币种和交易状态
             ↓
更新本地订单

签名算法、字段排序、字符编码和 Base64 规则都必须完全按照银行文档处理,不能只根据证书文件自行推断。

不要这样处理

下面的写法只能用于短时间排查问题,不能放进生产代码:

requests.post(url, verify=False)

它会关闭服务器身份验证,使连接面临中间人攻击的风险。即使传输内容仍然经过加密,也无法确认连接的确实是银行服务器。

也不要全局关闭 urllib3 的证书警告。隐藏警告并不会消除风险。

Django 中的实现注意事项

  • 银行地址、商户号和证书路径应放在环境配置中,不要硬编码敏感值。
  • 每个外部请求都要设置连接超时和读取超时。
  • 分别处理网络失败、HTTP 错误和银行业务错误。
  • 对暂时性网络错误进行重试时,必须继续使用同一订单号或幂等键,避免重复创建交易。
  • 应从当前请求中可靠地提取客户 IP。只有可信反向代理配置正确时,才能使用 X-Forwarded-For。
  • 示例代码中的 IP 与异常信息中的 IP 不一致,需要确认实际发送的是客户 IP,而不是复制的固定地址。
  • 如果第三方 POST 导致回调接口无法使用常规 CSRF 校验,可以只对该接口设置最小范围的例外,但仍然必须验证银行签名或共享密钥。
  • 证书更新前要检查到期时间,并为证书轮换预留时间。
  • 生产环境和测试环境的接口地址、商户凭据及证书通常相互隔离,不能混用。

如果正确配置银行提供的 CA 证书后仍然出现相同错误,应将证书链问题反馈给银行技术支持,请对方确认接口是否发送了完整证书链、证书适用于哪个环境,以及该端口是否要求 mTLS。不要长期关闭证书验证来掩盖服务端的配置问题。

备注:内容仅供参考。