防止表单重复提交几种方法+实测

18 篇文章 0 订阅

1.前端处理(场景:用于网络延迟情况下用户点击多次submit按钮导致表单重复提交)

①:通过一个标识来控制表单提交之后,再次提交会直接返回处理。

Var isCommitted = false;  //表单是否应提交标识,默认为false
function dosubmit() {
    //start hzj
	If(isCommitted == false){
	 //提交表单后,将表单是否已经提交设置为true
	isCommitted = true;
	 //返回true让表单正常提交
	return true;
	}else{
	return false; //返回false表单不提交
	  }
	//end hzj
	
}

**总结:**将以上的判断逻辑加在提交业务之前和业务处理之后都可以

②:通过点击提交一次按钮之后,将该按钮设置为不可用处理。

function dosubmit() {
//获取表单提交按钮

Var btnSubmit = documen.getElementById(“sumit”);

//将表单提交按钮设置为不可用,可以避免用户再次点击提交按钮进行提交

btnSubmit.disabled = “disabled”;

//返回true让表单可以提交

return true;

}

**总结:**将这个方法放到第一次提交按钮成功之后,会将按钮设置为不可点击状态。

2.后端处理(场景1:表单提交后,用户点击-刷新-按钮导致表单重复提交。场景2:用户表单提交后,点击浏览器的-后退-按钮,退到表单提交页面,再次提交按钮导致表单重复提交。)

①:给数据库增加唯一键约束(简单粗暴)

在数据库建表的时候在ID字段添加主键约束,用户名、邮箱、电话等字段加唯一性约束。确保数据库只可以添加一条数据。
数据库加唯一性约束sql:
alter table tableName_xxx add unique key uniq_xxx(field1, field2)
服务器及时捕捉插入数据异常:
		try {
	            xxxMapper.insert(user);
	        } catch (DuplicateKeyException e) {
	            logger.error("user already exist");
	        }

**总结:**针对给表中字段新增入库的情况

②:session技术实现
a.在服务器端生成一个唯一的随机标识号,专业术语称为Token(令牌),同时在当前用户的Session域中保存这个Token。
b.然后将Token发送到客户端的Form表单中,在Form表单中使用隐藏域来存储这个Token,表单提交的时候连同这个Token一起提交到服务器端。
c.然后在服务器端判断客户端提交上来的Token与服务器端生成的Token是否一致,如果不一致,那就是重复提交了,此时服务器端就可以不处理重复提交的表单。如果相同则处理表单提交,处理完后清除当前用户的Session域中存储的标识号。

//1.登录controller层
    String token = TokenProccessor.getInstance().makeToken();//创建令牌
    request.getSession().setAttribute("token", token);  //在服务器使用session保存token(令牌)
    return data;//返回成功数据,跳转到坐席端首页页面(将token放入data中,返回页面将token放入隐藏域中的表单标签中)
//2.前端页面表单中放入token
<input type="hidden" name="token" value="token值"/> 
//3.正真处理提交业务的逻辑层
boolean b = isRepeatSubmit(request);//判断用户是否是重复提交
             if(b==true){
                 System.out.println("请不要重复提交");
                 return;
             }
             request.getSession().removeAttribute("token");//移除session中的token
             System.out.println("处理用户提交请求!!");
         }
         
         /**
          * 判断客户端提交上来的令牌和服务器端生成的令牌是否一致
          * @param request
          * @return 
          *         true 用户重复提交了表单 
          *         false 用户没有重复提交表单
          */
         private boolean isRepeatSubmit(HttpServletRequest request) {
             String client_token = request.getParameter("token");
             //1、如果用户提交的表单数据中没有token,则用户是重复提交了表单
             if(client_token==null){
                 return true;
             }
            //取出存储在Session中的token
            String server_token = (String) request.getSession().getAttribute("token");
             //2、如果当前用户的Session中不存在Token(令牌),则用户是重复提交了表单
             if(server_token==null){
                 return true;
             }
             //3、存储在Session中的Token(令牌)与表单提交的Token(令牌)不同,则用户是重复提交了表单
             if(!client_token.equals(server_token)){
                 return true;
             }
             
             return false;
         }
//4.生成token的工具类
public class TokenProccessor {
  
     /*
      *单例设计模式(保证类的对象在内存中只有一个)
      *1、把类的构造函数私有
      *2、自己创建一个类的对象
      *3、对外提供一个公共的方法,返回类的对象
      */
     private TokenProccessor(){}
     
     private static final TokenProccessor instance = new TokenProccessor();
     
     /**
      * 返回类的对象
      * @return
      */
     public static TokenProccessor getInstance(){
         return instance;
     }
     
     /**
      * 生成Token
      * Token:Nv6RRuGEVvmGjB+jimI/gw==
      * @return
      */
     public String makeToken(){  //checkException
         //  7346734837483  834u938493493849384  43434384
         String token = (System.currentTimeMillis() + new Random().nextInt(999999999)) + "";
         //数据指纹   128位长   16个字节  md5
         try {
             MessageDigest md = MessageDigest.getInstance("md5");
             byte md5[] =  md.digest(token.getBytes());
             //base64编码--任意二进制编码明文字符   adfsdfsdfsf
             BASE64Encoder encoder = new BASE64Encoder();
             return encoder.encode(md5);
         } catch (NoSuchAlgorithmException e) {
             throw new RuntimeException(e);
         }
     }
 }

**总结:**使用session技术,是一个用户对应一个session容器

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

//首先自定义注解
/**
 * 避免重复提交
 * @author 
 * @version
 * @since
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AvoidRepeatableCommit {

    /**
     * 指定时间内不可重复提交,单位毫秒,默认10000毫秒
     */
    long timeout()  default 10000 ;
}
//创建切面类
/**
 * 重复提交aop
 */
@Aspect//
@Component
public class AvoidRepeatableCommitAspect {
	private static final Logger logger = Logger.getLogger(AvoidRepeatableCommitAspect.class);
    @SuppressWarnings("rawtypes")
	@Autowired
    private RedisTemplate redisTemplate;

    /** 
     * @param point 连接点
     */
    @SuppressWarnings("unchecked")
	@Around("@annotation(com.***.annotation.AvoidRepeatableCommit)")//切面拦截
    public Object around(ProceedingJoinPoint point) throws Throwable {
    	ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    	HttpServletRequest request = attributes.getRequest();
        String ip = IPUtil.getIP(request);
        //此处method获取的是代理对象(由代理模式生成的)的方法
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        //此处realMethod是目标对象(原始的)的方法
		// Method realMethod = point.getTarget().getClass().getDeclaredMethod(signature.getName(),method.getParameterTypes()); 
        		
        //目标类、方法
        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);
		//logger.info("ipKey={},hashCode={},key={}",ipKey,hashCode,key);
        logger.info(String.format("ipKey={},hashCode={},key={}",ipKey,hashCode,key));
        //通过反射技术来获取注解对象
        AvoidRepeatableCommit avoidRepeatableCommit =  method.getAnnotation(AvoidRepeatableCommit.class);
        long timeout = avoidRepeatableCommit.timeout();
        if (timeout < 0){
            //过期时间10秒
            timeout = 10000;
        }
        //获取key键对应的值
        String value = (String) redisTemplate.opsForValue().get(key);
        if (StringUtils.isNotBlank(value)){
            return new Message(1,"请勿重复提交!");
        }
        //新增一个字符串类型的值,key是键,value是值。
        redisTemplate.opsForValue().set(key, UUIDUtil.uuid(),timeout,TimeUnit.MILLISECONDS);
        
        //返回继续执行被拦截到的方法    
        return point.proceed();
    }

}

注意:
这个类有两个注释,分别是@Component和@Aspect,
@Component是使得AvoidRepeatableCommitAspect 受Spring托管并实例化。
@Aspect就是使得这个类具有AOP功能(你可以这样理解)两个注解缺一不可
类里面只有一个方法,名字叫做around,其实就是为了防重复提交的!

//在我需要防重复提交的方法上添加 自定义注解
// 新增
	@AvoidRepeatableCommit //自定义注解
	@RequestMapping(method = RequestMethod.POST)
	public @ResponseBody Message create(SourceEntity sourceEntity) {
		//设置创建时间
		sourceEntity.setGmt_create(new Date());
		//保存数据库
		sourceEntity.save(sourceEntity);
		return MessageUtil.message("sourceEntity.create.success");
	}

**总结:**到这里就完成了,主要步骤就是通过每次提交表单时,Aspect都会保存当前key到reids(先设置过期时间)。重复提交时Aspect会判断当前redis是否有该key,若有则拦截。没有就放行。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值