PAYATHON 2026

Flutter 中安全存储 API Keys 还是向 Server 发送支付信息?

支付阿杰

结论

不要把支付服务商的秘密 API Key 放进 Flutter 应用,也不要为了隐藏密钥,就默认让自己的 Server 接收银行卡号。

推荐采用以下架构:

  1. Server 保存 secret key,并向支付服务商创建短期、权限受限的支付会话。
  2. Flutter 只获取 publishable key、client token、client secret 等允许公开或短期使用的凭证。
  3. 用户通过支付服务商提供的官方 SDK、Payment Sheet 或托管组件输入卡信息。
  4. SDK 将卡号、CVV 等敏感数据直接发送给支付服务商。
  5. Flutter 和 Server 后续只处理 token、payment method ID、订单号和支付状态。

如果某家支付服务商要求客户端必须使用长期 secret key 才能完成 tokenization,那么它通常不适合直接集成到不可信的移动客户端。此时应使用该服务商的移动端 SDK 或临时凭证方案,也可以改用支持这类架构的接口。

为什么客户端不能保存 secret key

Flutter 应用最终会被编译并分发到用户设备。无论密钥写在 Dart 代码、.env、原生资源、Keychain 或 Android Keystore 中,还是经过混淆,只要应用能够读取和使用它,攻击者原则上也能通过反编译、动态调试或网络分析获取它。

Keychain 和 Android Keystore 适合保护用户登录 token、设备密钥等安装后生成或下发的数据,却不能让随安装包分发的共享 secret 真正保密。

泄露的支付 API Key 可能被用于:

  • 创建未经授权的交易或退款;
  • 查询或修改商户数据;
  • 绕过应用中的金额和业务规则;
  • 消耗接口配额;
  • 冒充商户调用其他高权限 API。

这里需要区分两类凭证:

  • publishable key:服务商明确允许放在客户端,通常只有有限权限;
  • secret key:只能保存在受控的 Server 环境中。

所谓“客户端不能存 API Key”,并不是所有名称中带有 key 的值都不能公开。判断依据是服务商对该凭证的权限定义。

推荐的数据流

Flutter ──订单信息──> Merchant Server
                         │
                         │ secret key
                         ▼
                  Payment Provider
                         │
                         │ client token / client secret
                         ▼
Flutter <────────短期支付会话──────── Merchant Server

Flutter ──卡信息,经官方 SDK 直接发送──> Payment Provider
Flutter <──────── payment method token ──────── Payment Provider

Flutter ──确认订单或 token──> Merchant Server
Merchant Server ──确认支付──> Payment Provider

金额、币种、商品和收款账户必须由 Server 自行确定。不要信任 Flutter 上传的最终金额,否则攻击者可以篡改客户端请求,用较低金额支付价格更高的订单。

Flutter 侧的实现思路

下面是一个不依赖具体支付服务商的示意代码。PaymentProviderSdk 代表服务商的官方 SDK,实际类名和参数应以所选服务商的文档为准。

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<void> startPayment(String orderId) async {
  final response = await http.post(
    Uri.parse('https://api.example.com/payment-sessions'),
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer <user-access-token>',
    },
    body: jsonEncode({
      'orderId': orderId,
    }),
  );

  if (response.statusCode != 201) {
    throw Exception('Unable to create payment session');
  }

  final data = jsonDecode(response.body) as Map<String, dynamic>;
  final clientToken = data['clientToken'] as String;

  // 示意:应使用支付服务商提供的官方 UI/SDK 收集卡信息。
  // 卡号和 CVV 不应通过自己的 HTTP 接口发送。
  await PaymentProviderSdk.presentPaymentSheet(
    clientToken: clientToken,
  );
}

不要采用下面这种设计:

await http.post(
  Uri.parse('https://api.example.com/tokenize-card'),
  body: jsonEncode({
    'cardNumber': cardNumber,
    'expiry': expiry,
    'cvv': cvv,
  }),
);

即使使用 HTTPS,卡信息仍会进入自己的 Server、框架、中间件和运行环境。TLS 只能保护传输过程,无法防止敏感数据被请求日志、异常追踪、APM、反向代理或调试代码记录。

Server 侧的实现思路

Server 应使用环境变量或专门的 secrets manager 保存 secret key,并为已经通过认证的用户创建短期支付会话。

app.post('/payment-sessions', requireUser, async (req, res) => {
  const order = await loadOrder(req.body.orderId, req.user.id);

  if (!order || order.status !== 'pending') {
    return res.status(400).json({ error: 'Invalid order' });
  }

  // 金额和币种来自服务端订单,不接受客户端指定的最终值。
  const session = await paymentProvider.createPaymentSession({
    amount: order.amount,
    currency: order.currency,
    reference: order.id,
  });

  res.status(201).json({
    clientToken: session.clientToken,
  });
});

这里的 paymentProvider.createPaymentSession 也是抽象示例。实际接口可能返回 client_secret、ephemeral key、checkout session ID 或其他短期凭证。

订单状态不能只根据 Flutter 返回的“支付成功”页面更新。支付是否成功,应以后端查询结果或经过签名验证的 webhook 为准。

对 PCI DSS 合规范围的影响

如果卡号先经过自己的 Server,再由 Server 转发给支付服务商,通常会显著扩大 PCI DSS 的适用范围。可能涉及的不只是接收卡号的接口,还包括:

  • 负载均衡器和反向代理;
  • 应用服务器及其依赖;
  • 日志、监控和异常追踪系统;
  • 网络分段与访问控制;
  • 部署流程、开发人员权限和密钥管理;
  • 备份、消息队列和故障排查工具。

因此,“只发送一次”并不能让相关系统脱离合规范围。系统只要能够接收、处理或传输持卡人数据,就可能受到相应控制要求的约束。

不过,把卡信息直接发给支付服务商,也不代表可以自动免除全部 PCI DSS 责任。如果 Flutter 自己绘制输入框、读取 PAN 或 CVV,再调用 tokenization API,应用代码和相关开发流程仍可能影响支付数据安全。使用服务商官方的托管支付页面或经过其验证的支付组件,通常更有利于缩小合规范围。

具体适用哪份 SAQ、移动应用是否需要纳入评估,以及 token 是否仍被视为敏感数据,取决于支付流程、服务商方案、收单机构要求和适用的 PCI DSS 版本。不能仅凭“数据没有经过 Server”自行判断合规类别,应由支付服务商、收单机构或 QSA 根据完整数据流确认。

需要特别注意的事项

  • 不要记录 PAN、CVV、磁道数据或完整的支付 SDK 请求。
  • 授权完成后不得保存 CVV,即使经过加密也不应保存。
  • 客户端只能使用服务商明确标注为 publishable 的凭证。
  • 临时客户端凭证应具有较短的有效期和有限的权限,并与用户或订单绑定。
  • Server 必须验证用户身份、订单归属、金额、币种和订单状态。
  • webhook 必须验证签名,并正确处理重复通知和乱序通知。
  • token 不一定天然脱离 PCI DSS 范围,需要确认它是否可逆、能否跨商户使用,以及服务商如何定义它。
  • 不要自行设计银行卡加密或 tokenization 协议,优先使用支付服务商的官方 SDK。
  • 上线前应绘制完整的数据流图,并检查日志、代理、崩溃报告和分析工具是否可能接触敏感字段。

总体原则是:secret key 留在 Server,卡信息直接进入支付服务商的受控组件,自己的系统只接触订单信息和无法直接还原卡号的支付标识。这样通常可以同时控制密钥泄露风险和 PCI DSS 合规范围。

备注:内容仅供参考。