当前市面上解决重复提交的技术方案主要有以下几种:
- 通过JavaScript限制表单重复提交
通过js代码,当用户点击提交按钮后,屏蔽提交按钮或者将重复点击在指定情况下置为无效,使用户无法点击提交按钮或点击无效,从而实现防止表单重复提交。
- 给数据库加唯一索引约束
在数据库建表的时候在ID字段添加主键约束,用户名、邮箱、电话等字段加唯一性约束。确保数据库只可以添加一条数据。
- 利用session进行防止表单重复提交
服务器返回表单页面时,会先生成一个subToken保存于session,并把该subToen传给表单页面。当表单提交时会带上subToken,服务器拦截器Interceptor会拦截该请求,拦截器判断session保存的subToken和表单提交subToken是否一致。若不一致或session的subToken为空或表单未携带subToken则不通过。
首次提交表单时session的subToken与表单携带的subToken一致走正常流程,然后拦截器内会删除session保存的subToken。当再次提交表单时由于session的subToken为空则不通过。从而实现了防止表单重复提交。
- 利用AOP标记重复提交表单信息,自定义不可重复提交时间
自定义重复提交注解,每次提交表单时Aspect会保存当前标记到redis,并对该缓存设置过期时间,当表单在该缓存过期之前提交则会被认为是重复提交
本方案着重解决无法完全拦截、增加数据库压力、老接口改动成本大、校验时间不灵活等问题,参考方案4,对利用AOP标记重复提交表单方案的基础上进行了优化处理,可以做到以注解的形式应用到所有需要防止表单的接口上,技术实现更轻量级,防重时间把控上随不同接口的响应速度而改变并且有效的减小了服务端与数据库的压力。并且在实际应用中起到了很好的效果。
1、利用AOP技术建立切面,自定义注解;
2、在注解实现中获取用户的所有请求参数与用户信息并按照一定规则对请求参数与用户信息进行排序并舍弃掉参数值为null的参数,以保证每次相同的提交请求进来的参数顺序都是相同的
3、对排序的参数进行一个拼接得到一个拼接的字符串,为了数据的安全性我们在此字符串的基础上加一个自定义的盐(java 盐),此时我们将加盐后得到字符串进行一个hash算法将此字符串处理成一个固定长度的字符串作为防重标识
4、以一个特定的字符串前缀加上第3步我们得到的防重标识作为一个redis的key,目的是日后便于维护查找所有防重复提交的key
5、对redis进行一个incr(key)原子操作得到一个结果锁lock,以保证此方案可以不被并发场景所影响
6、当lock等于1时表示此去请求为正常请求,可以继续往下进行,否则表示当前请求为重复提交请求。
7、当请求为正常请求时再针对该key设置一个托底过期时间,此时间只有在释放lock失败的场景下,此托底时间生效,当托底时间过期之后,redis会因为key过期而自动释放lock以避免该表单被永久锁住永远不可再次提交的情况
8、释放程序正常执行对应接口的逻辑操作并对该释放操作进行try与finally操作,在finally中对redis进行del(key)操作删除此缓存用以释放锁。同时对释放操作进行异常捕捉打印日志,做到出现问题时程序员第一时间知晓问题。此时接口执行完毕之后,自动释放防重复提交锁,下一次提交便可以正常进行。
且看伪代码:
@Around("antiDuplicationAccess()") public Object around(ProceedingJoinPoint pjp) throws Throwable { RequestAttributes ra = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes sra = (ServletRequestAttributes) ra; HttpServletRequest request = sra.getRequest(); Map<String, String> paramsMap = getRequestParams(request); String uid = request.getHeader("uid"); paramsMap.put("uid",uid); String sign = JceSecretUtil.hash(SignUtil.sort(paramsMap) + ApiConsts.SIGN_SALT, JceSecretUtil.HashTypeEnum.MD5.getValue()); if (StringUtils.isBlank(sign)) { return pjp.proceed(); } String curSign = ApiConsts.ANTI_DUPLICATION + sign; try{ // 防重复提交 long lock = Redis.incr(curSign); if (lock > 1) { //重复提交自定义处理方式,可抛异常 } Redis.expire(curSign, ApiConsts.SIGN_KEY_EXPIRE_TIME); }catch (JedisException ex){ logger.error("Redis Exception, incr or expire is Exception, sign:{},message:{}", curSign, ex); } try { return pjp.proceed(); } finally { try{ Redis.del(curSign); }catch (JedisException ex){ logger.error("Redis Exception, del is Exception, sign:{},message:{}", curSign, ex); } } }