美文网首页
SpringMVC的拦截器实现防重复提交不生效问题

SpringMVC的拦截器实现防重复提交不生效问题

作者: 一帅 | 来源:发表于2018-02-28 15:36 被阅读0次

    这次我们不谈怎么防止重复表单,有几种方案,方案优劣如何。这次我们只谈其中一种方案不生效的问题。

    问题描述

    过完年的后某一天

    测试小妹妹:客户反馈任务上报记录页面有重复的记录。我看了下,在线上测试了下,发现有重复提交问题。

    我:这。。。不可能啊,也不能啊。我们后端是做了重复提交的限制的。肯定是前端的问题,你去找一下前端小哥哥吧。

    半个小时后。。。

    前端小哥哥:我查过了,确实点快了是会有重复调用接口,但是我们给后端的HTTP头部是一样的。按道理后端应该屏蔽掉的。

    我:怎么可能呢。。。那这个问题在微信端和App上都有吗

    测试小妹妹:只有微信上有问题,APP上没有这个问题。

    我懵逼了。。。

    好了,我来简单讲一下我们是怎么做后端放重复提交表单数据的。我们是使用的SpringMVC的拦截器来实现的。下面是简单的伪代码

     public class TokenInterceptor extends HandlerInterceptorAdapter
    {
        private static Logger logger = LoggerFactory.getLogger(TokenInterceptor.class);
    
        @Resource
        private StokenHandler stokenHandler;
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
        {
            if (handler instanceof HandlerMethod)
            {
                HandlerMethod handlerMethod = (HandlerMethod) handler;
                Method method = handlerMethod.getMethod();
                Stoken annotation = method.getAnnotation(Stoken.class);
                if (annotation != null)
                {
                    boolean isCheckToken = annotation.check();
                    if (isCheckToken)
                    {
                        String submitToken = StringUtil.valueOf(request.getHeader("_token_"));
                        if (isRepeatSubmit(submitToken))
                        {
                            logger.warn("请不要重复提交数据,URL:" + request.getServletPath());
                            ApiResponse ret = ApiResponse.failed(0, "请不要重复提交数据");
                            ActionUtil.responseText(response, ret.toJSONString(), ActionUtil.CONTENT_TYPE_JSON);
                            return false;
                        }
                    }
                }
                return true;
            }
            else
            {
                return super.preHandle(request, response, handler);
            }
        }
    
        private boolean isRepeatSubmit(String submitToken)
        {
                    // 使用redis的setnx来防止并发问题
                    //  伪代码
            return redis.setNX(submitToken);
        }
    
        // set and get
    
    }
    
    

    可以看到我们的做法是对有注解Stoken的方法进行拦截,从Http头部中获取key为"token"的值。如果这个值不在redis中就认为没有重复。然后前端的处理是:在进入表单提交页面的时候就生成UUID,然后提交的时候放入头部"token"中。这样的话,如果是因为网路原因或者是用户点击比较快的话,"token"是一样的,后端会统一拦截,认为是重复提交数据。

    问题分析

    首先微信端的功能是上个版本才上线的,App上的功能早就上线了。两边后端接口是一样的。正常情况下不会有差异。然后我和前端一起在开发环境上重现了这个问题,我查看日志发现开发环境中微信端之所以防止重复提交不生效,是因为从http头部中没有获取到"token"的值。

    这就比较诡异了,抓包发现前端是传了这个头部的,但是后端却没有获取到

    然后比较巧合的是,前端在测试的时候不仅仅在手机微信上测试(点击微信菜单)了,他还在浏览器上用ip直接访问试了一下竟然是没有问题的,不会重复提交。

    这就比较尴尬了。我能想到的两者唯一的不同就是:

    一个是用域名访问的,一个是ip直接访问的
    PS:开发过微信公众号的同学就知道菜单上是用域名来访问的。不懂的同学可以参考微信网页授权

    难道使用域名就可以正确获取到头部,使用ip就不能正确获取头部吗?这不可能,这这两者到底有什么不同呢。

    域名是经过好多中间层转发的,而ip是直接访问服务器的。所以中间层(比如nginx)在转发http请求的时候丢掉了某些头部。

    然后上网一查,果然nginx自定义header头内容丢失

    解决问题

    • 方法一:不用下划线
      既然nginx对下划线不支持,那没关系,不用下划线就是了。比如原来”token”改成”-token-”就可以了。(难怪一般header的name都是’-‘来拼接的,比如”User-Agent”)

    • 方法二:从根本接触nginx的限制
      nginx默认request的header的那么中包含’_’时,会自动忽略掉。
      解决方法是:在nginx里的nginx.conf配置文件中的http部分中添加如下配置:
      underscores_in_headers on; (默认 underscores_in_headers 为off)

    最终我们选择了方案一,因为如果选择了方案二,那么程序就比较依赖nginx配置了,一旦换机器的话,那么很可能丢失配置。

    相关文章

      网友评论

          本文标题:SpringMVC的拦截器实现防重复提交不生效问题

          本文链接:https://www.haomeiwen.com/subject/xlabxftx.html