PCI Compliance 中 secured/hosted fields 的 WCAG 指南
结论
有。即使每个 secure field 分别符合表单和 iframe 的 WCAG 要求,也不能说明整个支付流程无障碍。
还需要验证这些跨文档字段组合使用时,用户能否理解字段之间的关系,按合理顺序操作,发现并修正错误,并顺利完成后续的 3-D Secure、身份验证和订单确认。PCI 合规关注支付数据安全和责任范围,不能代替 WCAG 一致性评估。
重点风险
1. 父页面与 iframe 不属于同一个可访问性文档
父页面中的文字不能直接充当跨域 iframe 内输入框的 <label>、aria-describedby 或错误说明。这些名称和说明必须放在 iframe 内,或者由支付服务提供商通过 API 同步进去。
父页面中的 iframe 也应有明确的可访问名称:
<div class="payment-field">
<iframe
src="https://payments.example/fields/card-number"
title="信用卡号安全输入框"
allow="payment"
></iframe>
</div>
title 用来说明嵌入区域的用途,不能代替 iframe 内输入框自己的 <label>。
2. 多个字段应当作为一个支付表单来理解
如果卡号、有效期和 CVV 分别位于不同的 iframe 中,屏幕阅读器可能将它们呈现为多个独立文档。测试时需要确认:
- 字段顺序是否与视觉顺序一致;
- 按 Tab 键时,焦点是否依次进入卡号、有效期、CVV 和提交按钮;
- 是否存在多余的焦点停靠点;
- 用户离开一个
iframe后,焦点能否可靠地进入下一个控件; - 页面是否使用了正数
tabindex。通常不应这样设置; - 页面更新、验证失败或重新渲染时,焦点是否会意外返回页面顶部。
不要拦截 Tab 键来强制切换字段。除非确有必要,否则应让浏览器按照 DOM 顺序管理焦点。
3. 错误应在正确的位置得到识别和播报
只显示红色边框是不够的。错误应有文字说明,并与对应的输入框建立程序化关联。
支付服务提供商控制的 iframe 内部可以采用类似结构:
<label for="card-number">信用卡号</label>
<input
id="card-number"
name="card-number"
type="text"
inputmode="numeric"
autocomplete="cc-number"
aria-describedby="card-number-hint card-number-error"
aria-invalid="true"
>
<p id="card-number-hint">
输入卡片正面的信用卡号。
</p>
<p id="card-number-error" role="alert">
信用卡号不完整,请检查后重新输入。
</p>
需要注意以下事项:
- 只有字段确实无效时,才应设置
aria-invalid; - 错误信息应告诉用户如何修正,不能只写“输入无效”;
- 不要只依赖颜色、图标或占位符传达错误;
- 用户尚未完成输入时,不宜反复播报错误;
- 提交失败后,应把焦点移到错误摘要或第一个无效字段;
- 错误摘要应与具体字段的错误信息一致。
跨域父页面通常不能直接控制 iframe 内的焦点,因此支付 SDK 需要提供 focus()、on("error")、on("validityChange") 等能力。实际 API 名称由供应商决定,不能假定所有 SDK 都支持这些方法。
4. 状态变化不能只发生在 iframe 内
支付字段可能处于加载、令牌化、验证失败、网络超时或服务不可用等状态。父页面需要在支付流程中向用户提供可感知的反馈:
<div id="payment-status" role="status" aria-live="polite"></div>
function updatePaymentStatus(message) {
document.getElementById("payment-status").textContent = message;
}
updatePaymentStatus("正在安全处理支付信息。");
一般状态可以使用 role="status"。只有需要用户立即处理的错误,才考虑使用 role="alert"。即时播报过多会干扰用户输入。
如果跨域文档通过 postMessage 传递状态,父页面必须验证消息来源:
window.addEventListener("message", (event) => {
if (event.origin !== "https://payments.example") {
return;
}
if (event.data?.type === "payment-field-error") {
updatePaymentStatus(event.data.message);
}
});
不要通过消息传递完整卡号、CVV 等敏感数据。具体通信方式还应符合支付服务提供商的安全设计和适用的 PCI 要求。
5. 自动填充和输入方式需要实际测试
支付字段应尽量使用正确的自动填充标记,例如:
autocomplete="cc-name"
autocomplete="cc-number"
autocomplete="cc-exp"
autocomplete="cc-csc"
浏览器、密码管理器和第三方 iframe 对自动填充的支持并不一致,跨域场景尤其如此。因此,不能只确认属性是否存在,还必须进行实际测试。
同时应检查:
- 移动设备是否显示合适的键盘;
- 卡号是否接受空格或连字符;
- 是否可以粘贴;
- 用户能否逐字修改,而不是只能清空后重新输入;
- 有效期格式是否明确;
- 是否为 CVV 的位置和含义提供了可访问的说明;
- 自动格式化是否会导致光标跳动,或让屏幕阅读器重复播报。
出于安全考虑,CVV 通常不应持久化或自动保存。具体行为应遵循支付服务提供商和适用的 PCI 要求。
6. 缩放、高对比度和自定义样式可能不起作用
iframe 内的样式不一定会继承父页面设置。即使父页面支持深色模式、高对比度或 200% 缩放,secure field 仍可能使用固定字号,出现对比度不足或内容被裁切的问题。
应检查:
- 文本放大后是否被截断;
- 在 320 CSS 像素宽度下是否需要双向滚动;
- 焦点指示是否清楚;
- 焦点是否会被固定页头、Cookie 横幅或底部操作栏遮挡;
- 浏览器使用强制颜色模式时,边框和错误状态是否仍然可见;
- 用户自定义字体、字距和行距后,标签及错误信息是否完整;
iframe高度能否随错误文字的增加而调整。
固定高度的 iframe 很容易裁掉错误信息。支付 SDK 最好提供安全的自动高度调整机制。
7. 测试范围不能止于 secure fields
完整的测试还应覆盖:
- 支付方式选择;
- 账单地址;
- 优惠码和订单金额更新;
- “提交订单”按钮的禁用状态;
- 加载遮罩;
- 支付失败后的恢复路径;
- 3-D Secure 或银行身份验证弹窗;
- 验证超时、取消和返回;
- 订单确认与支付结果;
- 重复提交和重复扣款预防。
对于第三方验证窗口,应重点检查焦点能否正确进入,焦点是否被合理约束,用户如何关闭窗口,以及关闭后焦点能否回到原来的位置。即使 secure fields 本身合格,只要后续身份验证界面无法操作,用户仍然不能完成购买。
支付或订单提交通常涉及金融或法律承诺。应根据适用的一致性级别评估 WCAG 的错误预防要求,例如在提交前提供检查和确认机会,或者允许用户撤销操作。
建议的验收方法
不要只审核静态 markup。至少应完成以下测试:
- 仅使用键盘完成一次成功支付,并完成一次支付失败后的错误修正。
- 使用屏幕阅读器逐个检查字段的名称、角色、状态、说明和错误信息。
- 在 Windows 强制颜色模式和浏览器高对比度设置下检查页面。
- 将页面放大到 200%,并在窄视口下测试重排和内容裁切。
- 使用移动端屏幕阅读器测试虚拟光标、软键盘和自动填充。
- 测试加载失败、网络超时、字段重建和支付服务不可用等情况。
- 测试 3-D Secure、银行验证,以及返回商户页面后的焦点位置。
- 使用真实浏览器与辅助技术组合进行测试,不能只依赖自动扫描工具。
自动化工具可以发现缺少 title、可访问名称或部分对比度问题,但通常无法判断跨 iframe 的操作顺序和播报时机,也无法验证错误恢复及第三方验证流程是否可用。
与支付服务提供商确认的能力
集成前,建议要求供应商明确说明:
- 如何命名
iframe和内部输入框; - 是否支持传入标签、说明和本地化错误文本;
- 是否公开有效性变化和错误事件;
- 是否提供聚焦无效字段的方法;
- 是否支持自动填充属性;
- 是否能够调整
iframe高度; - 是否支持缩放、强制颜色和用户样式;
- SDK 更新后是否会保留这些无障碍行为;
- 其合规声明覆盖哪些 WCAG 版本、级别、浏览器和辅助技术组合。
供应商的合规声明可以作为证据,但不能代替商户对最终集成页面的测试。最终是否符合 WCAG,取决于 secure fields、父页面、支付 SDK 和后续验证流程共同构成的实际用户体验。
备注:内容仅供参考。
