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

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

问题描述

过完年的后某一天

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

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

半个小时后。。。

前端小哥哥:我查过了,确实点快了是会有重复调用接口,但是我们给后端的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配置了,一旦换机器的话,那么很可能丢失配置。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 第一章 Nginx简介 Nginx是什么 没有听过Nginx?那么一定听过它的“同行”Apache吧!Ngi...
    JokerW阅读 32,856评论 24 1,002
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 175,008评论 25 709
  • 你说 我们一起去看夕阳 幸福吗 你说 我们一起去流浪 快乐吗 你听 那雪花飘落的声音 你听 那浪花中的涟漪耳呢 你...
    云水间浅阅读 1,267评论 0 2
  • 你说你要一场阅读 1,215评论 0 0
  • “三爷,三爷,……”李四拿着一把漂亮的宝剑,可是脸上的表情却是像是老婆被人抢走了一般。“扑通”、“当啷啷”宝剑和李...
    半朽阅读 3,335评论 11 27