Flutter 中安全存储 API Keys 还是向 Server 发送支付信息?
结论
不要把支付服务商的秘密 API Key 放进 Flutter 应用,也不要为了隐藏密钥,就默认让自己的 Server 接收银行卡号。
推荐采用以下架构:
- Server 保存 secret key,并向支付服务商创建短期、权限受限的支付会话。
- Flutter 只获取 publishable key、client token、client secret 等允许公开或短期使用的凭证。
- 用户通过支付服务商提供的官方 SDK、Payment Sheet 或托管组件输入卡信息。
- SDK 将卡号、CVV 等敏感数据直接发送给支付服务商。
- 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 合规范围。
备注:内容仅供参考。
