专栏首页开发架构二三事实战之防止表单重复提交

实战之防止表单重复提交

防止重复提交

对于防止重复提交,最简单也最不安全的做法相信大家也都经历过,前端在一个请求发送后立即禁用掉按钮,这里咱们来讨论一下后端对防止重复提交的处理方式。 主要针对非分布式环境下防止重复提交与分布式环境下的防止重复提交。一般分布式环境下也可以通过网关路由的方式将同一个用户的请求路由到一个实例上处理。

单进程内的防止重复提交

单个进程内防止重复提交可以选取的方式有很多种,因为并不是每一个接口都需要做防止重复提交的校验,所以在java中通常采用注解+拦截器的方式来实现。 另外一点就是,针对每一个接口的每一次请求都要有一个与之相对应的key来做去重操作。在当有一个key的请求正在处理时,另一个携带相同key的请求会被拒绝掉。key 的取值取决于系统对接口和资源的切分粒度。 废话不说,直接上代码:

1. 注解

@Documented@Inherited@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface SubmitPassport {
    boolean validate() default true;
    String methodName() default "";
    /**     * 0表示普通的rest接口,9表示html接口     * @return     */    int interfaceType() default 0;
    /**     * 用户单线程     * 会使用当前用户id做一个分布式锁来控制     * @return     */    boolean userSingleThread() default false;
    int time() default 5;

    enum InterfaceType{        /**         * 普通的         */        Normal(0),        /**         * ftl格式的         */        FTL(9);
        int code;
        InterfaceType(int code) {            this.code = code;        }
        public int getCode() {            return code;        }    }
}
  • 在每个有效用户访问时,平台会为该用户颁发一个token,这个token在pc端是放在cookie中的,移动端是放在header中的。
  • 在有些时候,这个token可以简单地使用ip来替换(针对一台交换机上过来的公网请求ip,这种方式会有问题)。
  • 在上面注解中是通过接口方法名与token或当前用户id来切分资源的。

拦截器里面的处理:

public class SubmitInterceptor extends HandlerInterceptorAdapter {
private static final Cache<String, Object> CACHES = CacheBuilder.newBuilder()            // 最大缓存 100 个            .maximumSize(1000)            // 设置写缓存后 5 秒钟过期            .expireAfterWrite(5, TimeUnit.SECONDS)            .build();
 @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)            throws IOException, ServletException {        if (handler.getClass().isAssignableFrom(HandlerMethod.class)) {            SubmitPassport submitPassport = ((HandlerMethod) handler).getMethodAnnotation(SubmitPassport.class);            // 没有声明需要权限,或者声明不验证权限            if (submitPassport == null || submitPassport.validate() == false) {                return true;            } else {                Object attribute = request.getAttribute(BaseGlobalConstants.CURRENT_USER);                if(submitPassport.userSingleThread() && attribute != null){                    //有些操作是需要在同一时间同一用户只能操作一次的 防止用户多浏览器登录的情况                    UserBaseTo userBaseTo = (UserBaseTo) attribute;                    Long userId = userBaseTo.getId();                    String key = submitPassport.methodName() + userId;                    if (CACHES.getIfPresent(key) != null) {                        throw new RuntimeException("请勿重复请求");                    }                }else{                    Cookie[] cookies = request.getCookies();                    String cookiesId = null;                    if (cookies != null) {                        for (Cookie cookie : cookies) {                            if (OpSysConstants.CSRF_TOKEN_KEY.equals(cookie.getName())) {                                cookiesId = cookie.getValue();                                break;                            }                        }                    }                    if (cookiesId == null) {                        String servletPath = request.getServletPath();                        String[] split = servletPath.split("/");                        if (MBEnum.ANDROID.getValue().equals(split[1]) || MBEnum.IOS.getValue().equals(split[1])) {                            cookiesId = request.getRemoteAddr();                        }else {                            throw new OpBusinessException(PublicExceptionCodeEnum.EX_ILLEGAL_REQUEST.getCode(),PublicExceptionCodeEnum.EX_ILLEGAL_REQUEST.getMsg());                        }                    }                    String key = submitPassport.methodName() + cookiesId;                     if (CACHES.getIfPresent(key) != null) {                        throw new RuntimeException("请勿重复请求");                     }                }                 try {                    return pjp.proceed();                } catch (Throwable throwable) {                    throw new RuntimeException("服务器异常");                } finally {                //处理完之后移除key                    CACHES.invalidate(key);                }         }     }     return true;  }                          

上面使用的是guava的cache作为容器来存放key的,当然还可以使用concurrentHashMap作为存放key的容器,其他缓存工具比如ehcache等也可以使用。

map操作获取和释放锁的操作如下:

 /**     * 获取object lock     *     * @param key     * @return     */    private boolean tryLock(String key) {        if (key == null) {            LOGGER.error(" ================the key can not be null");            return false;        }        String putIfAbsent = sessionIdMap.putIfAbsent(key, key);        if (putIfAbsent == null) {            return true;        }        return false;    }
    /**     * 释放锁     *     * @param key     */    private void releaseLock(String key) {        if (key != null) {            sessionIdMap.remove(key, key);        }    }

进程内防止重复提交的特点很明显,就是构建一个锁池,每个需要防止重复提交的请求需要来池中获取锁,每个请求处理完了之后会释放相应的锁,而锁的粒度是根据业务边界而定。

分布式环境下防止重复提交

和单进程的实现方式类似,只是这个锁池是分布式的,多个进程来这里申请锁,然后资源利用完之后会释放锁。没错,这就是传说中的分布式锁。其他的操作与单进程内的处理方式一样。关于redis实现分布式锁的几种方式和需要注意的点,请关注之后的文章。

本文分享自微信公众号 - 开发架构二三事(gh_d6f166e26398),作者:两个小灰象

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-06-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Elasticsearch源码分析八之锁管理工具KeyLock

    KeyedLock的map属性是存放资源标识和KeyLock的容器,也就是一个大的锁容器。KeyLock为每一个资源标识对应的锁对象,它继承自Reentrant...

    开发架构二三事
  • dubbo源码分析之filter加载机制

    然后在doExportUrlsFor1Protocol方法中会进行url的拼接操作,将一些参数拼接到url的后面,形式为ip:port/com.rt.Servi...

    开发架构二三事
  • golang小工具download公众号文章或其他网页图片

    直接运行main.go文件或者通过go build ./打成windows下的exe包或者在linux下打成downloadPic包直接运行

    开发架构二三事
  • 掌握 HashMap 看这一篇文章就够了

    最近几天,一直在学习 HashMap 的底层实现,发现关于 HashMap 实现的博客文章还是很多的,对比了一些,都没有一个很全面的文章来做总结,本篇文章也断断...

    纯洁的微笑
  • 奔跑吧! HashMap,值得你一阅!

    https://github.com/leosanqing/StructAndAlgorithm/tree/master/Struct/hashMapDemo

    用户5224393
  • Java源码阅读之TreeMap(红黑树) - JDK1.8

    TreeMap实现了NavigableMap接口, 而NavigableMap则是通过sortedMap间接继承了Map接口,它定义了一系列导航方法,这些Map...

    格子Lin
  • 通过一个实际案例,彻底搞懂 HashMap!

    我知道大家都很熟悉hashmap,并且有事没事都会new一个,但是hashmap的一些特性大家都是看了忘,忘了再记,今天这个例子可以帮助大家很好的记住。

    Java技术栈
  • 跳表(skiplist)的原理及concurrentskiplistmap的源码学习

    本文分为两个部分,第一个是对跳表(SKipList)这种数据结构的介绍,第二部分则是对Java中ConcurrentSkilListMap的源码解读.

    呼延十
  • 通过一个实际案例,彻底搞懂 HashMap

    我知道大家都很熟悉hashmap,并且有事没事都会new一个,但是hashmap的一些特性大家都是看了忘,忘了再记,今天这个例子可以帮助大家很好的记住。

    南风
  • Java集合源码解析 - HashMap

    HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长.

    JavaEdge

扫码关注云+社区

领取腾讯云代金券