排坑指南-异步操作HttpServletRequest丢失Cookie

  created  by  鱼鱼 {{tag}}
创建于 2020年11月10日 19:08:31 最后修改于 2020年11月11日 20:54:45

问题:携带cookie调用getCookie却返回null

    遇到了一个很奇怪的bug:请求鉴权失败,因为通过Request对象获取到的Cookie中没有数据。经过debug调用request.getCookies()方法返回了null值,但是header属性的cookie却能拿到用户的有效cookie(request.getHeader("cookie")),其中缘由,且慢慢道来。

Request中的Cookie对象缓存

    我们可以在web项目中通过Request对象很方便的获取Cookie对象:

Cookie[] cookies = request.getCookies();

    但其内部实现其实有一层缓存逻辑,从名为"cookie"的请求头中读取并处理数据转为Cookie对象并不是个省时事,在org.apache.catalina.connector.Request类中可以看到如下代码实现:

/**
 * Return the set of Cookies received with this Request. Triggers parsing of
 * the Cookie HTTP headers followed by conversion to Cookie objects if this
 * has not already been performed.
 *
 * @return the array of cookies
 */
@Override
public Cookie[] getCookies() {
    if (!cookiesConverted) {
        convertCookies();
    }
    return cookies;
}

    cookies已经作为一个对象属性存在其中,通过cookiesConverted判断是否已经首次解析过cookie,如果解析过则直接取旧值。因此一个Request对象的Cookie值原则上也是不能修改的。

    convertCookies方法实现,我们可以不用看解析cookies的实现:

/**
 * Converts the parsed cookies (parsing the Cookie headers first if they
 * have not been parsed) into Cookie objects.
 */
protected void convertCookies() {
    if (cookiesConverted) {
        return;
    }
    //标示已经获取过cookie
    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存入
            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;
    }
}

问题的本质—Request对象的复用

     Tomcat容器的线程模型是“一请求一线程“,其对象也是隔离的,但却不是每次新创建的Request对象,相比频繁的创建和销毁,显然在web应用中使用对象池读Request对象进行复用更加合理,这也就意味着我们所有对此request的操作都要遵循request本身的生命周期(请求响应返回后销毁)。这里涉及到的销毁实际是调用了Request的以下方法(其实就是赋了一堆null):

/**
 * Release all object references, and initialize instance variables, in
 * preparation for reuse of this object.
 */
public void recycle() {

    internalDispatcherType = null;
    requestDispatcherPath = null;
    authType = null;
    inputBuffer.recycle();
    usingInputStream = false;
    usingReader = false;
    userPrincipal = null;
    subject = null;
    parametersParsed = false;
    if (parts != null) {
        for (Part part: parts) {
            try {
                part.delete();
            } catch (IOException ignored) {
                // ApplicationPart.delete() never throws an IOEx
            }
        }
        parts = null;
    }
    partsParseException = null;
    locales.clear();
    localesParsed = false;
    secure = false;
    remoteAddr = null;
    remoteHost = null;
    remotePort = -1;
    localPort = -1;
    localAddr = null;
    localName = null;

    attributes.clear();
    sslAttributesParsed = false;
    notes.clear();

    recycleSessionInfo();
    //在这里回收cookie
    recycleCookieInfo(false);

    if (Globals.IS_SECURITY_ENABLED || Connector.RECYCLE_FACADES) {
        parameterMap = new ParameterMap<>();
    } else {
        parameterMap.setLocked(false);
        parameterMap.clear();
    }

    mappingData.recycle();
    applicationMapping.recycle();

    applicationRequest = null;
    if (Globals.IS_SECURITY_ENABLED || Connector.RECYCLE_FACADES) {
        if (facade != null) {
            facade.clear();
            facade = null;
        }
        if (inputStream != null) {
            inputStream.clear();
            inputStream = null;
        }
        if (reader != null) {
            reader.clear();
            reader = null;
        }
    }

    asyncSupported = null;
    if (asyncContext!=null) {
        asyncContext.recycle();
    }
    asyncContext = null;
}

    注意上面的重置行为实际只是重置了一部分内置属性,并没有重置request内容(比如cookies[]),而是在下次请求到来时赋值,这也是问题没有被注意到的原因。而调用的recycleCookie方法的实现:

protected void recycleCookieInfo(boolean recycleCoyote) {
    cookiesParsed = false;
    //标记没有生成过Cookie
    cookiesConverted = false;
    cookies = null;
    if (recycleCoyote) {
        getCoyoteRequest().getCookies().recycle();
    }
}

问题产生的原因

    如果只是正常读取请求中的数据,不会有任何问题,但是程序中为了不阻塞前端交互对于某些数据的处理使用了异步操作,好巧不巧在这些操作中引用了request对象(为了直接将cookie引用过来请求第三方),整体流程如下

    在第4步首次请求已经响应并重置request对象后,后台于第5步异步调用了getCookies方法,虽然request已经重置,但其请求内容(参数、header)仍在,所以在异步的线程逻辑中会重新解析并赋值cookies对象,并且将已经重置为false的cookiesConverted设置为false。

    当新请求到来时,其内置属性不会重新赋值,这使得cookiesConverted被保留了下来,但是内置的cookies数组却被清空了,因此我们跳过了是否要重新解析cookie的判断试图直接获取cookies对象,拿到了一个null。

    问题解决也很简单:将getCookie方法的操作放在同步线程中,传给异步线程的对象是cookie而不是request,或是在新请求中手动解析cookie(当然这违背了他设计的初衷)。

评论区
评论
{{comment.creator}}
{{comment.createTime}} {{comment.index}}楼
评论

排坑指南-异步操作HttpServletRequest丢失Cookie

排坑指南-异步操作HttpServletRequest丢失Cookie

问题:携带cookie调用getCookie却返回null

    遇到了一个很奇怪的bug:请求鉴权失败,因为通过Request对象获取到的Cookie中没有数据。经过debug调用request.getCookies()方法返回了null值,但是header属性的cookie却能拿到用户的有效cookie(request.getHeader("cookie")),其中缘由,且慢慢道来。

Request中的Cookie对象缓存

    我们可以在web项目中通过Request对象很方便的获取Cookie对象:

Cookie[] cookies = request.getCookies();

    但其内部实现其实有一层缓存逻辑,从名为"cookie"的请求头中读取并处理数据转为Cookie对象并不是个省时事,在org.apache.catalina.connector.Request类中可以看到如下代码实现:

/**
 * Return the set of Cookies received with this Request. Triggers parsing of
 * the Cookie HTTP headers followed by conversion to Cookie objects if this
 * has not already been performed.
 *
 * @return the array of cookies
 */
@Override
public Cookie[] getCookies() {
    if (!cookiesConverted) {
        convertCookies();
    }
    return cookies;
}

    cookies已经作为一个对象属性存在其中,通过cookiesConverted判断是否已经首次解析过cookie,如果解析过则直接取旧值。因此一个Request对象的Cookie值原则上也是不能修改的。

    convertCookies方法实现,我们可以不用看解析cookies的实现:

/**
 * Converts the parsed cookies (parsing the Cookie headers first if they
 * have not been parsed) into Cookie objects.
 */
protected void convertCookies() {
    if (cookiesConverted) {
        return;
    }
    //标示已经获取过cookie
    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存入
            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;
    }
}

问题的本质—Request对象的复用

     Tomcat容器的线程模型是“一请求一线程“,其对象也是隔离的,但却不是每次新创建的Request对象,相比频繁的创建和销毁,显然在web应用中使用对象池读Request对象进行复用更加合理,这也就意味着我们所有对此request的操作都要遵循request本身的生命周期(请求响应返回后销毁)。这里涉及到的销毁实际是调用了Request的以下方法(其实就是赋了一堆null):

/**
 * Release all object references, and initialize instance variables, in
 * preparation for reuse of this object.
 */
public void recycle() {

    internalDispatcherType = null;
    requestDispatcherPath = null;
    authType = null;
    inputBuffer.recycle();
    usingInputStream = false;
    usingReader = false;
    userPrincipal = null;
    subject = null;
    parametersParsed = false;
    if (parts != null) {
        for (Part part: parts) {
            try {
                part.delete();
            } catch (IOException ignored) {
                // ApplicationPart.delete() never throws an IOEx
            }
        }
        parts = null;
    }
    partsParseException = null;
    locales.clear();
    localesParsed = false;
    secure = false;
    remoteAddr = null;
    remoteHost = null;
    remotePort = -1;
    localPort = -1;
    localAddr = null;
    localName = null;

    attributes.clear();
    sslAttributesParsed = false;
    notes.clear();

    recycleSessionInfo();
    //在这里回收cookie
    recycleCookieInfo(false);

    if (Globals.IS_SECURITY_ENABLED || Connector.RECYCLE_FACADES) {
        parameterMap = new ParameterMap<>();
    } else {
        parameterMap.setLocked(false);
        parameterMap.clear();
    }

    mappingData.recycle();
    applicationMapping.recycle();

    applicationRequest = null;
    if (Globals.IS_SECURITY_ENABLED || Connector.RECYCLE_FACADES) {
        if (facade != null) {
            facade.clear();
            facade = null;
        }
        if (inputStream != null) {
            inputStream.clear();
            inputStream = null;
        }
        if (reader != null) {
            reader.clear();
            reader = null;
        }
    }

    asyncSupported = null;
    if (asyncContext!=null) {
        asyncContext.recycle();
    }
    asyncContext = null;
}

    注意上面的重置行为实际只是重置了一部分内置属性,并没有重置request内容(比如cookies[]),而是在下次请求到来时赋值,这也是问题没有被注意到的原因。而调用的recycleCookie方法的实现:

protected void recycleCookieInfo(boolean recycleCoyote) {
    cookiesParsed = false;
    //标记没有生成过Cookie
    cookiesConverted = false;
    cookies = null;
    if (recycleCoyote) {
        getCoyoteRequest().getCookies().recycle();
    }
}

问题产生的原因

    如果只是正常读取请求中的数据,不会有任何问题,但是程序中为了不阻塞前端交互对于某些数据的处理使用了异步操作,好巧不巧在这些操作中引用了request对象(为了直接将cookie引用过来请求第三方),整体流程如下

    在第4步首次请求已经响应并重置request对象后,后台于第5步异步调用了getCookies方法,虽然request已经重置,但其请求内容(参数、header)仍在,所以在异步的线程逻辑中会重新解析并赋值cookies对象,并且将已经重置为false的cookiesConverted设置为false。

    当新请求到来时,其内置属性不会重新赋值,这使得cookiesConverted被保留了下来,但是内置的cookies数组却被清空了,因此我们跳过了是否要重新解析cookie的判断试图直接获取cookies对象,拿到了一个null。

    问题解决也很简单:将getCookie方法的操作放在同步线程中,传给异步线程的对象是cookie而不是request,或是在新请求中手动解析cookie(当然这违背了他设计的初衷)。


排坑指南-异步操作HttpServletRequest丢失Cookie2020-11-11鱼鱼

{{commentTitle}}

评论   ctrl+Enter 发送评论