PAYATHON 2026

在 Pylons/Python 应用中安全处理 payment processing

支付老李

结论

Pylons/Python 应用在技术上可以直接接收信用卡号,再由服务器提交给支付网关,但一般不建议采用这种方式。

只要信用卡号经过你的页面、应用服务器、日志或数据库,系统就会被纳入更大的 PCI DSS 合规范围。更简单也更安全的做法,是使用支付网关提供的 Hosted Fields、嵌入式 iframe 或前端 Tokenization SDK。用户仍然可以留在你的页面中完成支付,但卡号会由浏览器直接发送给支付网关。你的服务器只接收一次性 token 或 payment method ID。

推荐的支付流程

常见的处理流程是:

  1. 后端创建订单,并生成内部 order_id。
  2. 支付页面加载支付网关的官方 JavaScript SDK。
  3. 卡号输入框由网关托管,或者由网关 SDK 采集卡片信息。
  4. 浏览器把卡片信息直接提交给支付网关。
  5. 网关返回一次性 token,例如 payment_method_id。
  6. 浏览器将 token 和 order_id 提交给 Pylons 应用。
  7. 后端读取数据库中的订单金额,并调用支付网关完成付款。
  8. 后端通过 webhook 确认最终支付状态。

用户不需要跳转到网关网站,信用卡号也不会经过你的 Python 应用。

前端示意

各家支付网关的 SDK 名称和参数不同。下面的代码只用于说明整体结构,不能直接当作某个网关的正式接口使用:

<form id="payment-form">
    <div id="gateway-card-field"></div>
    <button type="submit">立即支付</button>
</form>

<script src="https://gateway.example/sdk.js"></script>
<script>
const gateway = PaymentGateway.init({
    publicKey: "PUBLIC_KEY"
});

const cardField = gateway.createCardField();
cardField.mount("#gateway-card-field");

document
    .getElementById("payment-form")
    .addEventListener("submit", async (event) => {
        event.preventDefault();

        const result = await gateway.createPaymentMethod({
            card: cardField
        });

        if (result.error) {
            alert(result.error.message);
            return;
        }

        const response = await fetch("/payments/confirm", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "X-CSRF-Token": window.csrfToken
            },
            body: JSON.stringify({
                order_id: "ORDER_ID",
                payment_method_id: result.paymentMethod.id
            })
        });

        const payment = await response.json();

        if (payment.status === "succeeded") {
            window.location.href = "/payments/success";
        } else {
            alert("付款尚未完成,请按页面提示处理。");
        }
    });
</script>

其中最重要的一点是,gateway-card-field 必须由支付网关托管。不要通过普通的 <input name="card_number"> 收集卡号,再把它发送到自己的服务器,否则卡片数据仍然没有得到隔离。

Pylons/Python 后端示意

后端只接收 token,不接收卡号、有效期或 CVV:

import hmac
import json
import os

import requests
from pylons import request, response


class PaymentsController:

    def confirm(self):
        payload = request.json

        order_id = payload.get("order_id")
        payment_method_id = payload.get("payment_method_id")

        if not order_id or not payment_method_id:
            response.status_int = 400
            return json.dumps({"error": "invalid_request"})

        order = load_unpaid_order(order_id)

        if order is None:
            response.status_int = 404
            return json.dumps({"error": "order_not_found"})

        # 金额和币种必须从服务端订单读取,不能相信浏览器提交的数据。
        gateway_response = requests.post(
            "https://api.gateway.example/v1/payments",
            headers={
                "Authorization": "Bearer " + os.environ["PAYMENT_GATEWAY_SECRET"],
                "Idempotency-Key": order.payment_attempt_id,
                "Content-Type": "application/json",
            },
            json={
                "payment_method": payment_method_id,
                "amount": order.amount_minor,
                "currency": order.currency,
                "reference": order.id,
            },
            timeout=(3.05, 15),
        )
        gateway_response.raise_for_status()

        result = gateway_response.json()

        return json.dumps({
            "status": result["status"],
            "payment_id": result["id"],
        })

代码中的 URL、字段和认证方式都是占位示例。实际开发时,必须以所选支付网关的官方 SDK 或 API 文档为准。amount_minor 通常表示最小货币单位,例如人民币的分或美元的美分,具体规则仍需查看网关文档。

Webhook 才是最终状态依据

支付成功后,用户可能直接关闭页面,前端响应也可能被伪造。因此,不能只根据支付结果页将订单标记为已付款。后端应接收网关发来的 webhook,并验证签名:

import hashlib
import hmac
import json
import os

from pylons import request, response


class PaymentWebhooksController:

    def receive(self):
        raw_body = request.body
        received_signature = request.headers.get("X-Gateway-Signature", "")

        expected_signature = hmac.new(
            os.environ["PAYMENT_WEBHOOK_SECRET"].encode("utf-8"),
            raw_body,
            hashlib.sha256,
        ).hexdigest()

        if not hmac.compare_digest(
            received_signature,
            expected_signature,
        ):
            response.status_int = 401
            return "invalid signature"

        event = json.loads(raw_body.decode("utf-8"))

        if event["type"] == "payment.succeeded":
            mark_order_paid_once(
                order_id=event["data"]["reference"],
                payment_id=event["data"]["payment_id"],
            )

        response.status_int = 200
        return "ok"

不同网关使用的签名算法、签名内容、时间戳校验方式和请求头名称并不相同,不能直接照搬这个示例。实现时应严格遵循网关文档,同时处理重复通知、乱序通知和重放攻击。

如果必须由应用接收卡号

只有在业务确实无法使用 tokenization 或托管字段时,才应考虑让服务器直接处理卡片数据。至少需要做到以下几点:

  • 全链路使用正确配置的 HTTPS,禁止通过 URL、查询字符串或非加密连接传输卡号。
  • 按适用的 PCI DSS 要求完成评估、扫描和安全控制。
  • 对应用、反向代理、监控平台和异常追踪系统的日志进行脱敏。
  • 不在数据库、缓存、消息队列、Session、备份或分析系统中保留完整卡号。
  • 授权完成后不得保存 CVV/CVC/CID,即使经过加密也不能保存。
  • 如果确实需要保存 PAN,应使用专业密钥管理、字段级加密和严格的访问控制。展示时只保留必要的掩码信息,例如 **** **** **** 1234。
  • 为支付请求设置幂等键,避免网络重试导致重复扣款。
  • 在服务端校验订单金额、币种、商品和订单状态。
  • 使用 CSRF 防护、Content Security Policy、安全 Cookie,并做好依赖漏洞管理。
  • 限制后台和运维人员接触支付数据,同时保留可审计的访问记录。

仅仅加密数据库并不能解决问题,因为卡号在写入数据库之前,已经经过 Web 服务器、Python 运行时和日志链路。

还需要注意的边界

Hosted Fields 或 tokenization 通常能显著缩小 PCI DSS 的适用范围,但不会自动免除全部合规义务。究竟适用 SAQ A、SAQ A-EP 还是其他评估类型,取决于页面集成方式、支付流程、服务商资质和收单机构的要求。具体情况应向支付服务商或合格的 PCI 顾问确认。

Pylons 是较早期的 Python Web 技术栈。支付模块需要特别检查 Python、Pylons 相关组件、TLS 库和其他依赖包是否仍处于安全支持周期。如果现有系统难以升级,可以将支付后端拆分为一个规模较小、依赖可控的独立服务。

对大多数应用来说,更合适的方案是让用户留在自己的页面中,同时让卡片数据直接进入支付网关。Pylons 后端只处理 token、订单和支付状态。

备注:内容仅供参考。