如何安全收集并提交 Direct Debit 信息
结论
不要先建立一张要求字段完全匹配的数据库,再让 Direct Debit 服务商直接访问。银行资料也不能通过邮件、前端 JavaScript 或浏览器直接发送。
通常应优先使用服务商提供的托管开户页面(hosted payment/mandate page)或嵌入式安全组件。用户在服务商页面填写银行账户信息,你的网站只保存 customer_id、mandate_id、付款周期和状态等引用数据。只有 London & Zurich 明确要求商户自行采集资料时,才由你的后端接收表单,并通过其正式 API/SOAP 接口提交。
Direct Debit 通常需要银行账户名、account number、sort code 和 mandate 信息,与 debit card payment 不是一回事。除非服务商明确要求,否则不要采集卡号、有效期或 CVV。
先向服务商确认接入方式
现有的 inputs/outputs 文档更像是接口字段说明,未必是完整的实施指南。示例中的 GetScheduledFuturePaymentsRequest 从名称来看是查询未来付款的请求,不是创建客户或建立 Direct Debit mandate 的入口。
开发前,需要请 London & Zurich 提供或确认以下信息:
- 商户测试环境和正式环境地址;
- 身份验证方式及测试凭据的申请流程;
- 创建客户、创建 mandate 和创建付款计划分别对应哪个操作;
- 是否提供 hosted page、iframe、JavaScript SDK 或 tokenization;
- SOAP 的 WSDL 地址、版本和 XML namespace;
- 月付和每三个月付款所需的字段及日期规则;
- webhook/callback 的签名验证方式;
- mandate 成功、失败、取消、逾期和退款状态的通知方式;
- 是否需要显示 Direct Debit Guarantee;
- 测试银行账户、测试场景和上线认证流程;
- 敏感数据的保存期限和删除要求。
如果文档只有字段列表,却没有 endpoint、WSDL、认证方法和错误码,就无法据此安全实施。应先向服务商索取 integration guide,或联系其技术支持。
推荐的集成流程
安全性较高、维护成本较低的流程通常是:
用户选择付款周期
↓
你的后端创建会员注册记录
↓
你的后端向服务商申请 mandate session
↓
用户跳转到服务商托管页面并填写银行资料
↓
服务商完成校验并返回 mandate reference
↓
服务商通过签名 webhook 通知最终状态
↓
你的网站更新会员和付款状态
数据库只需保存业务需要的引用信息,例如:
users
id
name
email
memberships
id
user_id
billing_interval // monthly 或 quarterly
provider_customer_id
provider_mandate_id
mandate_status
next_payment_date
created_at
updated_at
不要保存 CVV。使用托管页面时,通常也不需要保存完整的 account number 或 sort code。如果业务页面需要显示账户信息,可以保存服务商返回的掩码,例如 ******1234。
付款周期应在服务端映射为明确的业务枚举,不能直接接受浏览器提交的任意金额或周期:
const plans = {
monthly: {
amountPence: 4000,
intervalMonths: 1
},
quarterly: {
amountPence: 12000,
intervalMonths: 3
}
};
实际金额、首次扣款日期和周期规则必须以合同及服务商文档为准。
托管页面的后端示例
下面是一个通用流程示例。其中的字段名和 URL 都是占位符,并不代表 London & Zurich 的真实 API。实施时必须替换为官方文档规定的内容。
import express from "express";
const app = express();
app.use(express.json());
const plans = {
monthly: { amountPence: 4000, intervalMonths: 1 },
quarterly: { amountPence: 12000, intervalMonths: 3 }
};
app.post("/api/direct-debit/session", requireLogin, async (req, res) => {
const plan = plans[req.body.billingInterval];
if (!plan) {
return res.status(400).json({ error: "Invalid billing interval" });
}
// API 凭据只能保存在服务器端。
const providerResponse = await fetch(
process.env.DD_PROVIDER_SESSION_URL,
{
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.DD_PROVIDER_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID()
},
body: JSON.stringify({
externalCustomerId: String(req.user.id),
amount: plan.amountPence,
intervalMonths: plan.intervalMonths,
returnUrl: "https://example.com/direct-debit/return"
})
}
);
if (!providerResponse.ok) {
// 不要把响应中的敏感内容直接写入日志或返回浏览器。
return res.status(502).json({ error: "Unable to create mandate session" });
}
const session = await providerResponse.json();
// redirectUrl 应当来自受信任的服务商响应。
res.json({ redirectUrl: session.redirectUrl });
});
浏览器收到 redirectUrl 后,再跳转到服务商页面。API key、SOAP 用户名、密码和签名密钥绝不能放入 HTML、移动应用包或前端 JavaScript。
用户从服务商页面返回,只能说明浏览器完成了跳转,不能据此直接激活会员。最终状态需要通过服务端查询,或由经过验签的 webhook 确认。
如果只能自行采集银行资料
如果服务商不提供托管页面,并要求你提交 SOAP 请求,表单应通过 HTTPS 发送到你自己的后端:
浏览器表单 → 你的 HTTPS 后端 → London & Zurich SOAP API
不能使用下面的流程:
浏览器 → 公开数据库
服务商 → 直接读取你的会员数据库
简化后的 HTML 表单可以写成:
<form method="post" action="/direct-debit/mandates">
<label>
Account holder
<input
name="accountHolder"
type="text"
autocomplete="name"
maxlength="100"
required
>
</label>
<label>
Sort code
<input
name="sortCode"
type="text"
inputmode="numeric"
autocomplete="off"
pattern="[0-9 -]{6,8}"
required
>
</label>
<label>
Account number
<input
name="accountNumber"
type="text"
inputmode="numeric"
autocomplete="off"
pattern="[0-9]{8}"
required
>
</label>
<select name="billingInterval" required>
<option value="monthly">Monthly</option>
<option value="quarterly">Every 3 months</option>
</select>
<input type="hidden" name="csrfToken" value="SERVER_GENERATED_TOKEN">
<button type="submit">Set up Direct Debit</button>
</form>
字段格式必须按照服务商的要求实现。不能仅根据这个示例,假定所有账户都使用相同的长度或校验规则。
后端收到资料后,应立即验证并提交,尽量不要先写入业务数据库。生成 XML 时应使用成熟的 SOAP/XML 库,避免手动拼接字符串:
const payload = {
MerchantReference: membership.id,
AccountHolderName: accountHolder,
SortCode: normalizedSortCode,
AccountNumber: normalizedAccountNumber,
BillingInterval: billingInterval
};
const result = await directDebitClient.CreateMandateAsync(payload);
真实的 operation 名称、对象结构、namespace 和认证头必须从 WSDL 及服务商文档中获取。不要根据示例猜测 CreateMandate 等接口名称。
如果所用语言支持根据 WSDL 生成客户端,应优先生成强类型客户端。例如,.NET 通常可以使用 Visual Studio 的 Connected Services 或 dotnet-svcutil。这种方式是否适用于该接口,仍取决于服务商提供的 WSDL。
SOAP 集成需要特别注意的事项
使用 SOAP 并不代表接口自然就是安全的。实际安全性取决于传输、认证、证书校验和数据处理方式。
至少需要做到:
- 只连接服务商提供的固定 HTTPS endpoint;
- 正常校验 TLS 证书,不能关闭证书验证;
- 将凭据保存在 secret manager 或受保护的环境配置中;
- 如果服务商要求 WS-Security、客户端证书或 IP allowlist,严格按照文档配置;
- 使用库生成 XML,并确保 namespace 和字符转义正确;
- 设置连接超时和读取超时;
- 网络超时后,通过安全的重试和状态查询机制处理;
- 使用唯一的 merchant reference 或 idempotency key,避免重复建立 mandate;
- 不在日志中记录完整的 SOAP request/response;
- 将错误信息脱敏后再返回前端;
- 不允许用户控制 SOAP endpoint,以免产生 SSRF 风险;
- 校验服务商响应的结构,不能直接信任其中的跳转地址或状态值。
发生超时时,不要简单地再次创建 mandate,因为第一次请求可能已经成功。应使用唯一引用查询原请求的结果,或按照服务商提供的幂等机制重试。
Webhook 必须验签
状态变化通常会异步发生。服务商可能稍后通知 mandate 已确认、付款失败或付款被取消。收到 webhook 后,必须先验签,再更新数据库。
app.post(
"/webhooks/direct-debit",
express.raw({ type: "application/json" }),
async (req, res) => {
const signature = req.get("X-Provider-Signature");
if (!verifyProviderSignature(
req.body,
signature,
process.env.DD_WEBHOOK_SECRET
)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
if (await webhookAlreadyProcessed(event.id)) {
return res.sendStatus(200);
}
await processProviderEvent(event);
await markWebhookProcessed(event.id);
res.sendStatus(200);
}
);
X-Provider-Signature、签名算法和 event 字段都只是示意。实施时必须使用 London & Zurich 规定的 header、原始请求体和验证算法,同时处理重复通知、乱序通知和暂时性数据库故障。
必须落实的安全措施
传输和应用安全
- 所有注册、登录和付款页面都强制使用 HTTPS;
- 启用 HSTS,并为 session cookie 设置
Secure、HttpOnly和适当的SameSite; - 为表单启用 CSRF 防护;
- 对所有输入执行服务端白名单验证;
- 实施速率限制并监控异常请求;
- 后端只能访问服务商指定的地址;
- 生产、测试和开发环境使用不同的凭据;
- 定期轮换 API key、密码和 webhook secret。
数据最小化
只收集建立 mandate 必需的信息。如果电话号码、地址或出生日期不是服务商或合规流程要求的字段,就不要因为以后可能有用而收集。
如果确实必须临时保存银行账户资料,应采取以下措施:
- 使用受管理的密钥进行字段级加密;
- 将密钥与数据库分开保存;
- 严格限制后台、客服和开发人员的访问权限;
- 展示账户信息时进行掩码处理;
- 建立访问审计、保留期限和自动删除机制;
- 确保备份、日志、错误追踪系统和分析平台不会复制明文资料。
普通哈希不适用于需要再次提交给服务商的账户资料,因为哈希无法还原。需要还原时应使用加密;如果不再需要原值,就不要保存。
日志和监控
银行资料很容易通过调试日志意外泄露,需要检查以下位置:
- HTTP access log 是否记录 request body 或 query string;
- APM、错误追踪和 session replay 是否捕获表单内容;
- SOAP 调试日志是否记录完整 envelope;
- 客服后台是否显示完整账户;
- 数据库快照和备份是否受到同等保护。
日志只应保留内部会员编号、服务商引用、状态、错误码和必要的时间信息。
PCI DSS 与数据保护责任
如果实际采集的是银行卡号、有效期或 CVV,这属于 card payment 场景,需要按照支付服务商的要求评估 PCI DSS 范围。CVV 不得在授权后保存。
如果采集的是 Direct Debit 银行账户信息,通常不属于传统的银行卡数据处理,但这些资料仍是敏感的个人和金融信息。你仍需遵守适用的数据保护法规、Bacs 规则、服务商合同以及组织自身的安全义务。具体责任取决于所在司法辖区、商户角色和服务商的接入模式,应请服务商的合规团队或具备资质的顾问确认。
实施顺序
实际落地时,可以按以下顺序进行:
- 确认要收集的是 Direct Debit 银行账户资料,而不是 debit card details。
- 向 London & Zurich 索取完整的 onboarding/integration guide、测试环境、WSDL 和凭据。
- 优先选择 hosted mandate page 或 tokenized component。
- 在后端建立服务商适配层,不能让浏览器直接持有 API 凭据。
- 数据库只保存业务状态和服务商引用。
- 实现并验证 monthly 和 quarterly 两种计划的映射。
- 实现 webhook 验签、幂等处理和状态同步。
- 完成日志脱敏、权限控制、密钥管理和数据删除策略。
- 在测试环境中覆盖成功、拒绝、重复提交、超时、逾期、取消和 webhook 重放等场景。
- 通过服务商要求的上线审核后,再切换到正式 endpoint 和正式凭据。
这类集成不存在一个能够安全套用于所有供应商的通用 SOAP 请求。开始实施前,不应急着创建数据库,而要先确认服务商支持的正式接入模式,并尽可能让敏感银行资料直接进入服务商控制的环境。
备注:内容仅供参考。
