tomcat多线程传入HttpServletRequest导致cookie失效

2,154 阅读4分钟

最近线上出现了一个问题,客户端反应用户会异常退出。然后开始前后端一起排查问题,后端日志发现,该请求的cookie为null,导致获取用户信息,返回客户端请登录。

上面的是线上的日志。前面是servletRequest.getHeader("cookie");打印出来的内容illegal cookie里打印的是servletRequest.getCookies();的内容。很奇怪,header里面有。而request.getCookies()却拿不到东西。同事开始在本地复现,却难以复现。于是我开始和他一起解决这个问题。

本地debug走起

        <dependency>

            <groupId>org.apache.tomcat</groupId>

            <artifactId>tomcat-catalina</artifactId>

            <version>8.5.30</version>

            <scope>provided</scope>

        </dependency>

上面的maven配置可以让你能够调试tomcat源码,注意版本号和你的本地的tocmat版本一致。

首先在上图所示的位置的打了个断点。如果uid没有取到一定会进来,模拟了几次请求后,居然真的进来了。同事的本地无法复现,我这里却复现了。当时就想,能复现,就成功了一半。

果然,getCookie()是null,而getHeader有值。 为了弄明白cookie到底是在哪一步丢的常规思路是啥,先搞清楚它是那一步被init的。

    public Cookie[] getCookies() {
        if (!cookiesConverted) {
            convertCookies();//我设置断点的地方
        }
        return cookies;
    }
看上面的代码,如果cookie没有被convert就会covertCookies

    protected void convertCookies() {
        if (cookiesConverted) {
            return;
        }

        cookiesConverted = true;

        if (getContext() == null) {
            return;
        }

        parseCookies();

        ServerCookies serverCookies = coyoteRequest.getCookies();
        CookieProcessor cookieProcessor = getContext().getCookieProcessor();

        int count = serverCookies.getCookieCount();
        if (count <= 0) {
            return;
        }

        cookies = new Cookie[count];

        int idx=0;
        for (int i = 0; i < count; i++) {
            ServerCookie scookie = serverCookies.getCookie(i);
            try {
                /*
                we must unescape the '\\' escape character
                */
                Cookie cookie = new Cookie(scookie.getName().toString(),null);
                int version = scookie.getVersion();
                cookie.setVersion(version);
                scookie.getValue().getByteChunk().setCharset(cookieProcessor.getCharset());
                cookie.setValue(unescape(scookie.getValue().toString()));
                cookie.setPath(unescape(scookie.getPath().toString()));
                String domain = scookie.getDomain().toString();
                if (domain!=null)
                 {
                    cookie.setDomain(unescape(domain));//avoid NPE
                }
                String comment = scookie.getComment().toString();
                cookie.setComment(version==1?unescape(comment):null);
                cookies[idx++] = cookie;
            } catch(IllegalArgumentException e) {
                // Ignore bad cookie
            }
        }
        if( idx < count ) {
            Cookie [] ncookies = new Cookie[idx];
            System.arraycopy(cookies, 0, ncookies, 0, idx);
            cookies = ncookies;
        }
    }
    
    上面的这段代码是具体解析cookie的

这个断点进入了两次。有个子线程居然也进入了这个断点!!!乍一看好像也没啥,一个异步任务将request传到了子线程,并且也使用了获取cookie的方法。但是,只要子线程进入了这个方法,下一次接口请求就一定会提示登录,也就是没获取到cookie。由此我断定cookie的丢失肯定和这个东西有关。而且子线程的这个断点有时候会进去,有时候不会进去。只要进去了下次请求必然获取不到cookie。

回过头去看上面的代码,cookiesConverted为false则会重新covertCookies()。不然直接return,为啥子线程这个对象cookiesConverted被标记为false了呢!!主线程会把他标记为true的啊。我之前没有研读过tomcat的源码,但此时了解生命周期的人一定会有一定的猜想。那就是这个request对象的生命周期其实已经结束了,结束的时候会有一个清理的操作。把这个对象需要被清空的内容清空,方便对象池复用!所以子线程在getCookies的时候这个对象的生命周期理应当被结束了!!而子线程缺错误的把convert属性标记为true。下一个请求进来复用这个对象,由于看到你的标记为true,就不会给你解析cookie。

if (!cookiesConverted) {
            convertCookies();//我设置断点的地方
        }
        return cookies;

下面画个图,来支撑我的观点,方便大家理解

false true代表那个区间的cookiesConverted属性

主线程派生出来的子线程在执行getCookie()的情况可以分为两种。 第一种在主线程业务操作期间执行了getCookie(),这种情况不会有问题。

第二种情况在主线程执行clear操作之后把cookiesConverted标记为false的情况下又执行了getCookie()操作,这时就出现了问题,他把cookiesConverted又标记为true了。这时候对象池里的这个对象被下个请求拿到的时候它的cookiesConverted为true,自然不会解析cookie导致获取到cookie为null。所以HttpServletRequest对象不要传给子线程使用,因为在tocmat一个请求的生命周期结束之后这个对象就不应该再被别的地方所修改了。

最后当然就是需要验证下我的猜想,就需要仔细研读下这部分代码,最后证明下来,猜想并不完全正确,tomat的源码更加复杂,并不简单的是一个对象池。有兴趣的同学可以去研读下,这里就不展开了。但是对象肯定是复用了,而且线上这个问题也修复了。

最后的最后还有一个问题, 为啥同事的本地不能复现呢。

这是因为这个项目开启异步的mode是aspectj。而本地开启这个模式需要在vm options下添加-javaagent:/Users/nali/.m2/repository/org/aspectj/aspectjweaver/1.8.6/aspectjweaver-1.8.6.jar。由此可见保证线下线上的环境一致多么重要。