前言
某一天,老大突然把我叫到座位,指着屏幕。我一看,运维群,惊了!难道有事故,有bug!?情况是钱包系统数据有异常,后台同事查证同一笔手工批量充值发起了两次,怀疑可能是前端没有控制好,重复发起了批量充值请求。吓的我赶紧检查相关代码,发现在前端“点击按钮之后会有弹窗二次提醒用户是否操作”。那肯定就不是我的锅了。
如果没有弹窗二次确认,那在界面上不小心点击多次,真的就会发起多次请求,说不定第二天就要去财务室结算啦。由此可见,防止重复请求多么的重要!
事故场景
你可能要问了,如何预防呢?不要急,先看看在那些场景会导致重复请求: 1. 手速快,不小心双击操作按钮。 2. 很小心的点击了一次按钮,因为请求响应比较慢,页面没有任何提示,怀疑上次点击没生效,再次点击操作按钮。 3. 很小心的点击了一次按钮,因为请求响应比较慢,页面没有任何提示,刷新页面,再次点击操作按钮。
前端方案
我们可以对症下药: 1. 控制按钮,在短时间内被多次点击,第一次以后的点击无效。 2. 控制按钮,在点击按钮触发的请求响应之前,再次点击无效。 3. 配置特殊的URL,然后控制这些URL请求的最小时间间隔。如果再次请求跟前一次请求间隔很小,弹窗二次提示,是否继续操作。
防止无意识重复点击按钮
给按钮添加控制,在control
毫秒内,第一次点击事件之后的点击事件不执行。
<template>
<button @click="handleClick"></button>
</templage>
<script>
export default {
methods: {
handleClick(event) {
if (this.disabled) return;
if (this.notAllowed) return;
// 点击完多少秒不能继续点
this.notAllowed = true;
setTimeout(()=>{
this.notAllowed = false;
}, this.control)
this.$emit('click', event, this);
}
}
}
</script>
当然时间间隔可以设置,默认为300毫秒。我们无意识的重复点击一般在300毫秒以内。
按钮点击立马禁用,等响应回来才能继续点击
具体效果看如下GIF
![040d8ec28ad85fb9fc203d99ba4cc2d6.png](https://i-blog.csdnimg.cn/blog_migrate/5600ace4ac4b235d248d1df48765030a.png)
触发点击的button实例传入fetch配置,代码如下:
doQuery: function (button) {
this.FesApi.fetch(`generalcard/query`, {
sub_card_type: this.query.sub_card_type,
code_type: this.query.code_type,
title: this.query.title,
card_id: this.query.card_id,
page_info: {
pageSize: this.paginationOption.page_info.pageSize,
currentPage: this.paginationOption.page_info.currentPage
}
}, {
//看这里,加上下面一行代码就行。。so easy
button: button
}).then(rst => {
// 成功处理
});
}
在fetch函数内部,设置button的disabled=true
,当响应回来时,设置disabled=false
代码如下:
const action = function (url, data, option) {
// 如果传了button
if (option.button) {
option.button.currentDisabled = true;
}
// 记录日志
const log = requsetLog.creatLog(url, data);
return param(url, data, option)
.then(success, fail)
.then((response) => {
requsetLog.changeLogStatus(log, 'success');
if (option && option.button) {
option.button.currentDisabled = false;
}
return response;
})
.catch((error) => {
requsetLog.changeLogStatus(log, 'fail');
if (option && option.button) {
option.button.currentDisabled = false;
}
error.message && window.Toast.error(error.message);
throw error;
});
};
从根本入手,一招击杀
当页面刷新,页面状态重置,此时再次点击按钮,会判定为初次点击,而且按钮状态恢复可点击。我们可以设置哪些请求地址是重要的,它们请求间隔不能过小。如果过小,页面弹出覆层询问用户时候继续执行。
![ac11d3690f674ea305d9c13a8e178a5b.png](https://i-blog.csdnimg.cn/blog_migrate/40ecebd1bd928839c55bd347e7d546da.jpeg)
设置代码如下:
this.FesApi.setImportant({
'generalcard/action': {
control: 10000,
message: '您在十秒内重复发起手工清算操作,是否继续?'
}
})
而实现代码如下:
api.fetch = function (url, data, option) {
if (requsetLog.importantApi[url]) {
const logs = requsetLog.getLogByURL(url, data);
if (logs.length > 0) {
const compareLog = logs[logs.length - 1];
if (compareLog.status === 'compare') {
requsetLog.creatLog(url, data, 'notAllowed');
return {
then: () => {}
};
}
const importantApiOption = requsetLog.importantApi[url];
const control = importantApiOption.control || 10000;
const message = importantApiOption.message || util.format('fesMessages.importInterfaceTip', { s: control / 1000 });
if (new Date().getTime() - compareLog.timestamp < control) {
const oldStatus = compareLog.status;
requsetLog.changeLogStatus(compareLog, 'compare');
return new Promise(((resolve, reject) => {
window.Message.confirm(util.format('fesMessages.tip'), message).then((index) => {
if (compareLog.status === 'compare') {
requsetLog.changeLogStatus(compareLog, oldStatus);
}
if (index === 0) {
resolve(action(url, data, option));
} else {
reject(new Error('不允许相同操作间隔过小'));
}
});
}));
}
return action(url, data, option);
}
return action(url, data, option);
}
return action(url, data, option);
};
攻击者可以绕过正常流程,模拟发起多次请求,所以仅仅在前端页面做好预防重复请求工作是不够的。后台接口需要设计得更健壮,具有幂等性。
广告
Fes.js内置这三种预防重复请求的能力,是一个管理台应用解决方案,提供初始项目、开发调试、编译打包的命令行工具,内置布局、权限、数据字典、状态管理、Api等多个模块,文件目录结构即路由,用户只需要编写页面内容。基于Vue.js,内置管理台常用能力,让用户写的更少,更简单。经过多个项目中打磨,趋于稳定。
Fes.js开源项目地址如下,欢迎大家提交issue及star:
- Gitee地址:https://gitee.com/WeBank/fes.js
- Github地址:https://github.com/WeBankFinTech/fes.js