欢迎来到我的博客小站。  交流请加我微信好友: studyjava。  也欢迎关注同名公众号:Java学习之道

防止表单重复提交的前后端方法总结(@AvoidRepeatableCommit注解) 置顶!

  |   0 评论   |   0 浏览

原因:

前台操作的抖动,快速操作,网络通信或者后端响应慢,都会增加后端重复处理的概率。

情形

  • 由于用户误操作,多次点击表单提交按钮。
  • 后台返回数据延迟而产生的按钮重复提交
  • 由于网速等原因造成页面卡顿,用户重复刷新提交页面。
  • 黑客或恶意用户使用postman等工具重复恶意提交表单(攻击网站)。
  • 这些情况都会导致表单重复提交,造成数据重复,增加服务器负载,严重甚至会造成服务器宕机。因此有效防止表单重复提交有一定的必要性。

解决方案:

思路一(前端层面):

前端页面当用户点击按钮时,时按钮置灰,不允许继续点击按钮,从而防止在延迟时间内的重复提交。

缺点:js代码容易被绕过,如进行刷新或者F12后修改置灰按钮属性。

思路二(前端层面):

前端js文件设置一个全局变量,初始值为0,点击按钮一次则将变量+1,接收到返回值时将变量-1,在调用接口前判断变量是否为1,不为1则不调用接口,相当于一个开关,0=开 1=关。

思路三(数据库层面):

数据库添加唯一约束条件,如用户名、邮箱、电话等以确保数据库只能添加一条数据。

数据库添加唯一性约束条件sql:

alter table tableName_xxx add unique key uniq_xxx(field1, field2)

这种方法需要在代码中加入捕捉插入数据异常:

try {
    // insert
} catch (DuplicateKeyException e) {
    logger.error("user already exist");
}

缺点:虽然简单粗暴,能有效避免重复数据插入,但是无法阻止用户恶意重复提交表单,服务器大量增加插入sql语句的执行,增加数据库和服务器的负荷。

思路四(后台层面):

使用Redis和AOP自定义切入实现:

  • 自定义防止重复提交标记(@AvoidRepeatableCommit)。
  • 对需要防止重复提交的Congtroller里的mapping方法加上该注解。
  • 新增Aspect切入点,为@AvoidRepeatableCommit加入切入点。
  • 每次提交表单时,Aspect都会保存当前key到reids(须设置过期时间)。
  • 重复提交时Aspect会判断当前redis是否有该key,若有则拦截。

自定义标签:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 避免重复提交
 * @author lz
 * @version
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AvoidRepeatableCommit {
    /**
     * 指定时间内不可重复提交,单位秒
     * @return
     */
    long timeout()  default 5 ;
}

自定义切入点Aspect:

package com.xx.web;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;

import com.alibaba.fastjson.JSON;
import com.xx.web.util.DynamicRedisUtil;
import com.xx.web.util.IPUtil;

/**
 * 重复提交aop
 * 
 * @author 
 * @date 
 */
@Aspect
@Component
@EnableAspectJAutoProxy(exposeProxy=true)
public class AvoidRepeatableCommitAspect {


    @Autowired 
    HttpServletRequest request; //这里可以获取到request

    /**
     * @param point
     */
    @Around("@annotation(com.xx.web.AvoidRepeatableCommit)")
    public Object around(ProceedingJoinPoint point) throws Throwable {

        String ip = IPUtil.getIpAddr(request);
        //获取注解
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        //目标类、方法
        String className = method.getDeclaringClass().getName();
        String name = method.getName();
        String ipKey = String.format("%s#%s",className,name);
        int hashCode = Math.abs(ipKey.hashCode());
        String key = String.format("%s_%d",ip,hashCode);
//        log.info("ipKey={},hashCode={},key={}",ipKey,hashCode,key);
        AvoidRepeatableCommit avoidRepeatableCommit =  method.getAnnotation(AvoidRepeatableCommit.class);
        int timeout = avoidRepeatableCommit.timeout();
        if (timeout < 0){
            timeout = 5;
        }
        //用多参数set方法保证对redis操作原子性
        Integer isSuccess = DynamicRedisUtil.setnxAndExpire(key, UUID.randomUUID().toString(), timeout*1000, DynamicRedisUtil.AVOID_REPEATABLE_COMMIT_DB);
           if (isSuccess == 0) {
               resultMap.put("errCode", 10001);
               resultMap.put("errMsg", "请勿重复提交");
               return JSON.toJSONString(resultMap);
           }
        //执行方法
        Object object = point.proceed();
        return object;
    }

}

/**
 * redis工具类方法
 * 比setnx多了个保存失效时间
 * @author lz
 * @date 2018年8月13日 下午2:38:07
 * @param key
 * @param value
 * @param seconds 失效时间,单位秒
 * @param db
 * @return 当key不存在,保存成功并返回1,当key已存在不保存并返回0
 */
public static Integer setnxAndExpire(final String key, String value, long milliseconds, int db) {
    JedisPool pool = getPool();
    Jedis jds = null;
    boolean broken = false;
    int setnx = 0;
    try {
        jds = pool.getResource();
        jds.select(db);
        String result = jds.set(key, value, "NX", "PX", milliseconds);
        if ("OK".equals(result)) {
            setnx = 1;
        }
        return setnx;
    } catch (Exception e) {
        broken = true;
        logger.error("setString:" + e.getMessage());
    } finally {
        if (broken) {
            pool.returnBrokenResource(jds);
        } else if (jds != null) {
            pool.returnResource(jds);
        }
    }
    return setnx;
}

测试(使用):

@RequestMapping(value = "testCommit",method = {RequestMethod.GET,RequestMethod.POST})
@ResponseBody
@AvoidRepeatableCommit(timeout = 5)
public String testCommit(HttpServletRequest request){
    Map<String,Object> resultMap = new HashMap<String,Object>();
    try{    
        resultMap.put("success", true);
    }catch (Exception e) {
        e.printStackTrace();
        resultMap.put("success", false);
    }
    return JSON.toJSONString(resultMap);
}

思路五(后台层面):

分布式锁详情见他人博客,推荐:

原文:https://blog.csdn.net/memmsc/article/details/80837996

最后跪求各位大佬,路过点赞~谢谢大家!

参考


标题:防止表单重复提交的前后端方法总结(@AvoidRepeatableCommit注解)
作者:mmzsblog
地址:https://www.mmzsblog.cn/articles/2022/06/17/1655429359940.html
-----------------------------
如未加特殊说明,此网站文章均为原创。
若本站转载文章遗漏了原文链接,请及时告知,我们将做删除处理!
网站转载须在文章起始位置标注作者及原文连接,否则保留追究法律责任的权利。
公众号转载请联系网站首页的微信号申请白名单!

个人微信公众号 ↓↓↓                 

微信搜一搜 Java 学习之道