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

2018-02-28  本文已影响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配置了,一旦换机器的话,那么很可能丢失配置。

上一篇下一篇

猜你喜欢

热点阅读