如何安全实现 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、服务端鉴权、一次性会话和支付机构令牌化。
备注:内容仅供参考。
