iPhone 自动识别短信时重复填写支付宝验证码
结论
这类情况通常不是支付宝重复发送了验证码,而是页面同时处理了 iPhone 的短信验证码自动填充事件和普通输入事件,或者在事件回调中以“追加字符”的方式更新输入框,结果将同一验证码写入了两次。
处理时应统一验证码输入框的监听入口,始终用输入框的完整值覆盖业务状态,同时对重复事件和重复提交进行幂等处理。
原因分析
iPhone 自动填充验证码时,可能会一次写入完整内容,并触发 input、change 等事件。事件的具体触发顺序会因 iOS 版本、浏览器内核和页面实现而有所不同。
以下情况容易造成重复填写:
- 同时监听
input、change、keyup等多个事件,并在每个回调中写入验证码。 - 原生事件与 Vue、React 等框架事件同时更新同一个字段。
- 使用
currentCode += value追加内容,没有直接覆盖原值。 - 每次组件渲染或弹窗打开时都重新绑定事件。
- 自动填充完成后,脚本又读取短信内容并再次回填。
- 验证码填满后,多次触发支付确认或表单提交。
推荐处理方式
1. 正确声明验证码输入框
使用 autocomplete="one-time-code",可以提示 iOS 将该输入框识别为一次性验证码输入框:
<input
id="smsCode"
name="smsCode"
type="text"
inputmode="numeric"
autocomplete="one-time-code"
maxlength="6"
/>
maxlength 要根据验证码的实际长度设置,不能认为所有支付宝支付场景都使用 6 位验证码。
2. 只监听 input 事件
不要在多个事件中重复执行相同的验证码更新逻辑:
const codeInput = document.getElementById('smsCode');
codeInput.addEventListener('input', (event) => {
const code = event.target.value.replace(/\D/g, '').slice(0, 6);
// 使用完整值覆盖,不要使用 += 追加
event.target.value = code;
updateVerifyCode(code);
});
错误示例:
let verifyCode = '';
codeInput.addEventListener('input', (event) => {
verifyCode += event.target.value;
});
当自动填充一次写入完整验证码时,这段错误逻辑可能会重复追加同一组数字。
3. 对重复事件进行幂等处理
如果现有业务暂时无法合并事件监听,可以保存最近一次处理的完整值:
let lastCode = '';
function handleVerifyCode(event) {
const maxLength = 6;
const code = event.target.value
.replace(/\D/g, '')
.slice(0, maxLength);
event.target.value = code;
if (code === lastCode) {
return;
}
lastCode = code;
updateVerifyCode(code);
}
codeInput.addEventListener('input', handleVerifyCode);
这里应比较完整的验证码,而不是判断事件类型。即使 iPhone 连续触发两次相同事件,业务逻辑也只处理一次。
4. 防止重复绑定监听器
组件重新渲染、弹窗再次打开或页面局部刷新时,应先移除旧的监听器:
function mountCodeInput() {
const input = document.getElementById('smsCode');
input.removeEventListener('input', handleVerifyCode);
input.addEventListener('input', handleVerifyCode);
}
function unmountCodeInput() {
const input = document.getElementById('smsCode');
if (input) {
input.removeEventListener('input', handleVerifyCode);
}
}
removeEventListener 无法准确移除之前注册的匿名函数,因此这里应使用具名函数。
5. 防止重复提交支付请求
如果验证码自动填满后,页面会立即发起验证或支付请求,还需要增加防重复提交控制:
let submitting = false;
async function submitPayment(code) {
if (submitting) {
return;
}
submitting = true;
try {
await verifyAndPay(code);
} finally {
submitting = false;
}
}
前端防重只能改善交互。服务端仍需通过订单号、请求号或业务幂等键拦截重复支付请求,不能只依靠禁用按钮或 JavaScript 状态。
框架中的处理示例
以 Vue 为例,不要同时使用 v-model 和额外事件来重复拼接数据:
<input
:value="verifyCode"
type="text"
inputmode="numeric"
autocomplete="one-time-code"
maxlength="6"
@input="handleInput"
/>
function handleInput(event) {
verifyCode.value = event.target.value
.replace(/\D/g, '')
.slice(0, 6);
}
如果已经使用 v-model,额外的 @input 回调只用于校验或格式化,不应再次追加相同内容。
排查方法
可以临时记录相关事件,判断重复值来自浏览器事件还是业务代码:
['beforeinput', 'input', 'change', 'keyup'].forEach((eventName) => {
codeInput.addEventListener(eventName, (event) => {
console.log(eventName, {
value: event.target.value,
inputType: event.inputType,
data: event.data,
time: Date.now()
});
});
});
排查时主要检查以下几项:
- 一次自动填充触发了哪些事件。
- 同一个处理函数是否注册了多次。
- 输入框中的值是否正确,而业务变量却被重复拼接。
- 页面是否同时使用了框架绑定和原生 DOM 绑定。
- 验证码填满后是否发出了两次接口请求。
注意事项
不要依赖固定的 iOS 事件触发顺序,也不建议使用 setTimeout、判断设备型号或关闭 iPhone 自动填充来绕过问题。这些做法很容易受到系统升级和浏览器环境变化的影响。
如果重复内容出现在支付宝托管页面或支付宝提供的内嵌控件中,而且商户页面没有操作对应的输入框,商户侧通常无法直接修改。此时需要保存复现机型、iOS 版本、浏览器类型、支付链路、录屏和请求日志,再通过支付宝技术支持渠道核实。仅凭重复填写这一现象,无法断定问题一定出在 iOS 或支付宝。
备注:内容仅供参考。
