如何在 Django 网站中集成银行支付系统并解决 SSL 错误
结论
这类支付一般分为两个阶段:
- Django 后端调用银行支付接口,获取交易 ID、会话令牌或支付页面地址。
- 浏览器跳转到银行托管的支付页面,客户直接输入银行卡信息。支付完成后,银行通过返回地址、后台通知或查询接口反馈交易结果。
当前错误出现在第一阶段的 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 - 证书是否允许用于服务器身份认证
当前环境无法将银行返回的证书链连接到本机信任的根证书,因此验证失败。常见原因包括:
- 银行使用私有 CA 或自签名 CA,需要单独信任银行提供的 CA 证书。
- 银行服务器没有发送完整的中间证书链。
- 银行提供的是测试环境证书,而系统默认信任库并不信任该证书。
- 网络中的代理、防火墙或安全设备替换了 HTTPS 证书。
- 当前配置使用了错误、过期或不匹配的证书文件。
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。不要长期关闭证书验证来掩盖服务端的配置问题。
备注:内容仅供参考。
