Skip to content

怎么预防按钮的重复点击?

参考答案:

先看看在那些场景会导致重复请求:

  1. 手速快,不小心双击操作按钮。
  2. 很小心的点击了一次按钮,因为请求响应比较慢,页面没有任何提示,怀疑上次点击没生效,再次点击操作按钮。
  3. 很小心的点击了一次按钮,因为请求响应比较慢,页面没有任何提示,刷新页面,再次点击操作按钮。

前端方案

我们可以对症下药:

  1. 控制按钮,在短时间内被多次点击,第一次以后的点击无效。
  2. 控制按钮,在点击按钮触发的请求响应之前,再次点击无效。
  3. 配置特殊的URL,然后控制这些URL请求的最小时间间隔。如果再次请求跟前一次请求间隔很小,弹窗二次提示,是否继续操作。

防止无意识重复点击按钮

给按钮添加控制,在control 毫秒内,第一次点击事件之后的点击事件不执行。

text
<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毫秒以内。

按钮点击立马禁用,等响应回来才能继续点击

触发点击的button实例传入fetch配置,代码如下:

js
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代码如下:

js
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;
        });
};

从根本入手,一招击杀

当页面刷新,页面状态重置,此时再次点击按钮,会判定为初次点击,而且按钮状态恢复可点击。我们可以设置哪些请求地址是重要的,它们请求间隔不能过小。如果过小,页面弹出覆层询问用户时候继续执行。

设置代码如下:

js
this.FesApi.setImportant({
    'generalcard/action': {
        control: 10000,
        message: '您在十秒内重复发起手工清算操作,是否继续?'
    }
})

而实现代码如下:

js
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);
};

攻击者可以绕过正常流程,模拟发起多次请求,所以仅仅在前端页面做好预防重复请求工作是不够的。后台接口需要设计得更健壮,具有幂等性。

题目要点:

前端解决方案

  1. 防抖(Debounce):在用户停止操作后一定时间(例如300毫秒)内,如果再次操作,则忽略新操作。
  2. 节流(Throttle):限制函数在一定时间内只能执行一次,比如每300毫秒执行一次。
  3. 控制按钮状态:在按钮被点击后,立即禁用按钮,直到异步操作完成。
  4. 设置请求间隔限制:如果用户在短时间内多次发起相同请求,提示用户确认是否继续。

后端解决方案

  1. 幂等性(Idempotence):确保一个操作可以被安全地执行多次,每次执行结果都是相同的。例如,删除操作可以是幂等的,因为多次删除同一项将只删除一次。
  2. 唯一请求标识:为每个请求生成一个唯一的标识符(如UUID),并在后续请求中携带这个标识符。如果后端检测到重复的请求,可以拒绝处理。
  3. 请求限流:在一段时间内,限制同一用户或IP地址可以发起的请求数量。
  4. 使用缓存:对于某些类型的请求,可以缓存结果,并检查请求是否是重复的。如果是,则返回缓存结果。