PAYATHON 2026

如何安全实现 authorized net payment 数据传输?

支付阿杰

结论

不要把支付金额、银行卡信息、身份凭证、访问令牌等敏感数据拼接到 URL 中。Android 应通过 HTTPS POST 请求将数据发送给可信后端,由后端负责鉴权、校验以及与支付机构交互。如果后续必须打开 Web 页面,URL 中只能携带一个短时有效、仅可使用一次且不可预测的会话标识,再由 Web 页面通过后端安全获取对应状态。

需要说明的是,把参数从 URL 移到 POST 请求体,只能避免参数出现在地址栏、浏览器历史和常规访问日志中。真正保证传输机密性的是 HTTPS。客户端加密也不能替代 TLS 和服务端安全设计。

推荐的数据流

Android App
    │  HTTPS POST + Authorization
    ▼
业务后端
    │  校验用户、金额、订单状态
    │  创建支付会话
    ▼
支付服务商
    │
    ▼
Android 打开支付 Web 页面
    └─ URL 仅包含一次性 session_id

应遵循以下原则:

  • Android 不直接保存支付机构的私钥或长期密钥。
  • 银行卡号、CVV 等数据优先交给合规的支付 SDK 处理并令牌化。
  • 服务端必须根据订单重新确认金额和收款方,不能信任客户端提交的值。
  • URL 中只放没有业务含义的随机标识,不放敏感数据或可重复使用的凭证。
  • 所有接口只允许通过 HTTPS 访问,服务端还要验证用户身份和请求权限。

Android 使用 HTTPS POST

下面以 OkHttp 为例。实际项目中的域名、认证方式和字段需要根据后端协议调整:

import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject

private val client = OkHttpClient()

fun createPaymentSession(
    accessToken: String,
    orderId: String,
    callback: (String?, Exception?) -> Unit
) {
    val json = JSONObject()
        .put("order_id", orderId)
        .toString()

    val body = json.toRequestBody(
        "application/json; charset=utf-8".toMediaType()
    )

    val request = Request.Builder()
        .url("https://api.example.com/v1/payment-sessions")
        .header("Authorization", "Bearer $accessToken")
        .header("Accept", "application/json")
        .post(body)
        .build()

    client.newCall(request).enqueue(object : okhttp3.Callback {
        override fun onFailure(call: okhttp3.Call, e: java.io.IOException) {
            callback(null, e)
        }

        override fun onResponse(
            call: okhttp3.Call,
            response: okhttp3.Response
        ) {
            response.use {
                if (!it.isSuccessful) {
                    callback(
                        null,
                        IllegalStateException("HTTP ${it.code}")
                    )
                    return
                }

                val responseJson = JSONObject(it.body?.string().orEmpty())
                callback(responseJson.getString("payment_url"), null)
            }
        }
    })
}

后端可以返回类似的结果:

{
  "payment_url": "https://pay.example.com/session/T8x3mVnPq7..."
}

其中,T8x3mVnPq7... 应是高强度随机、短时有效且只能使用一次的标识,不能编码订单金额、用户身份或银行卡数据。

后端创建支付会话

下面的 Node.js 示例仅用于说明基本结构,不是完整的生产实现:

import crypto from "node:crypto";
import express from "express";

const app = express();
app.use(express.json());

app.post("/v1/payment-sessions", authenticateUser, async (req, res) => {
  const { order_id: orderId } = req.body;

  // 必须从服务端数据库读取并校验订单,不能直接相信客户端传来的金额。
  const order = await findPayableOrder(orderId, req.user.id);

  if (!order) {
    return res.status(404).json({ error: "ORDER_NOT_FOUND" });
  }

  const sessionId = crypto.randomBytes(32).toString("base64url");

  await savePaymentSession({
    sessionId,
    userId: req.user.id,
    orderId: order.id,
    expiresAt: new Date(Date.now() + 5 * 60 * 1000),
    used: false
  });

  res.set("Cache-Control", "no-store");
  return res.status(201).json({
    payment_url: `https://pay.example.com/session/${sessionId}`
  });
});

Web 页面收到 sessionId 后,服务端应检查:

  • 会话是否存在,并且尚未过期;
  • 会话是否已被使用;
  • 当前用户是否有权访问对应订单;
  • 订单金额、币种和收款方是否仍然有效;
  • 当前支付状态是否允许继续操作。

支付完成后,应立即使会话失效,避免链接被重复使用。

如果 Android 直接加载 Web 页面

使用 WebView 时,不要把敏感 JSON 拼接到查询参数中:

错误:
https://pay.example.com/start?card=...&amount=...&token=...

正确:
https://pay.example.com/session/random-one-time-id

WebView.postUrl() 可以把数据放入请求体,但它只能解决参数不显示在 URL 中的问题,无法自动处理身份认证、重放攻击、日志泄露和支付合规。支付流程通常更适合由后端创建会话。

以下做法也不能用于隐藏数据:

  • Base64 编码;
  • URL 编码;
  • 把数据放入 URL fragment(#);
  • 自行使用可逆加密,再把密文放入 URL;
  • 在 Android APK 中嵌入固定加密密钥或支付私钥。

Base64 和 URL 编码都不是加密。嵌入 APK 的固定密钥也可能被逆向提取。

必须补充的安全措施

服务端鉴权

每次创建或查询支付会话时,服务端都应验证用户身份。访问令牌应放在 Authorization 请求头中,不能放进 URL:

Authorization: Bearer <access_token>

令牌应当短时有效,同时避免将其写入应用日志、崩溃报告或分析平台。

防止重放和重复扣款

支付创建接口应支持幂等键:

Idempotency-Key: 73d44e8e-8fd4-4baf-b86a-38d54f9a7fcb

服务端需要保存幂等处理结果,保证使用同一个键重复发起请求时,不会创建多笔支付。一次性会话还应设置明确的过期时间和使用状态。

不信任客户端金额

客户端可以提交 order_id,但最终金额、币种、优惠和收款账户必须由服务端根据数据库中的数据计算。例如,不能只根据下面的客户端数据直接扣款:

{
  "amount": 1,
  "currency": "CNY"
}

Android 应用可能被篡改。HTTPS 能保护传输过程,但不能保证客户端提交的业务数据可信。

控制日志与缓存

即使数据放在请求体中,也可能被应用日志、代理、APM 或调试工具记录。生产环境应做到:

  • 不记录银行卡号、CVV、完整令牌和支付请求体;
  • 对账户、手机号等字段进行脱敏;
  • 为支付接口返回 Cache-Control: no-store;
  • 不把敏感内容明文写入 SharedPreferences 文件或剪贴板;
  • 生产版本不能使用会输出网络正文的调试拦截器。

使用支付服务商的令牌化方案

如果支付流程涉及银行卡数据,应优先使用支付服务商提供的 Android SDK、托管收银台或支付组件。通常由支付服务商直接采集卡信息,并向应用返回 token。业务服务器只处理 token,不接触原始卡号和 CVV。

是否需要遵守 PCI DSS,以及具体适用范围,取决于实际支付链路、地区和服务商方案,需要由合规或安全人员结合业务情况确认。

TLS 与证书校验

必须使用证书有效的 HTTPS,不能信任所有证书,也不能关闭主机名验证。证书固定(certificate pinning)可以降低部分中间人攻击的风险,但会增加证书轮换和灾难恢复的难度。只有具备可靠的轮换机制时,才建议启用。

如果需要对消息本身进行额外签名或端到端加密,应使用成熟协议,并由服务端安全保存密钥,不要自行设计加密算法。对大多数支付应用来说,首先需要落实的是正确配置 HTTPS、服务端鉴权、一次性会话和支付机构令牌化。

备注:内容仅供参考。