HttpClient 带Cookie请求的一个Bug

欢迎光临我的个人博客:https://www.jelliclecat.cn/

一、情景复现

最近在使用HttpClient做rpc的时候,需要带上Cookie做认证,是一个很简单的功能,官网上有标准做法:

public static String Get(String url, String ticket) throws IOException {
    CookieStore cookieStore = new BasicCookieStore();

    BasicClientCookie cookie = new BasicClientCookie(cookie_name, ticket);
    cookie.setDomain(".zrj.com");   // 示意domain
    cookie.setPath("/");
    cookieStore.addCookie(cookie);

    CloseableHttpClient httpClient = HttpClients.custom()
        .setDefaultCookieStore(cookieStore)
        .build();

    final HttpGet httpGet = new HttpGet(url);
    CloseableHttpResponse response = httpClient.execute(httpGet);
    int statusCode = response.getStatusLine().getStatusCode();
    if (statusCode > 299 || statusCode < 200) {
      throw new IOException("statusCode error : " + statusCode);
    }
    HttpEntity entity = response.getEntity();
    String responseStr = IOUtils.toString(entity.getContent());
    EntityUtils.consume(entity);
    response.close();
    httpClient.close();
    return responseStr;
}

发现服务的provider认证始终不能通过,服务端调试的时候,发现request里面没有携带任何cookie,确定不是服务的问题。那就可能是我们的HttpClient客户端没有发送cookie,google了一堆,没有发现任何问题,大部分博文中提到的标准写法就是上面的这种。

实在没有办法了,只能自己DEBUG,看看设置的CookieStore最后到底怎么着了。

二、DEBUG过程

在google的过程中,也不是完全没有收获,其中有一段话:

HttpClient的cookie最终都转换成了header保存在request中,但是于直接setHeather不同的是,使用CookieStore设置的Cookies会经过各种合规性校验。

这里看起来是我们解决问题的切入点,看看CookieStore最后是怎么转换为Header的。


    CloseableHttpResponse response = httpClient.execute(httpGet);

这里打断点进去,具体跟踪断点的方法不在这里赘述,我们的目标是找到CookieStore转换为Header的逻辑。

一直到ProtocolExec中:

this.httpProcessor.process(request, context);

debug的过程中可以看到HttpClient的大致逻辑,各种http请求的参数和设置都保存在context中,可以看到我们的CookieStore也在里面,还有Cookie的Spec集合,如果我们不设置cookie协议,会自动设置为"default":

[站外图片上传中...(image-77de1e-1559399169649)]

继续进去,发现关键逻辑:

// ImmutableHttpProcessor.java
    @Override
    public void process(
            final HttpRequest request,
            final HttpContext context) throws IOException, HttpException {
        for (final HttpRequestInterceptor requestInterceptor : this.requestInterceptors) {
            requestInterceptor.process(request, context);
        }
    }

这段代码使用各种不同的解释器,将context中的各种参数解析成标准的http格式,放在request中。

[站外图片上传中...(image-58ec7c-1559399169649)]

dubug模式下看一下RequestInterceptors列表,发现了一个RequestAddCookies实例,毫无疑问这就是将CookieStore转换为Header的解释器,进去看。

这个类里面,前面为我们将CookieSpecs设置成了"default":

        // RequestAddCookies.java
        final RequestConfig config = clientContext.getRequestConfig();
        String policy = config.getCookieSpec();
        if (policy == null) {
            policy = CookieSpecs.DEFAULT;
        }
        if (this.log.isDebugEnabled()) {
            this.log.debug("CookieSpec selected: " + policy);
        }

继续往后就看到了我们的关键代码:

        // RequestAddCookies.java
        for (final Cookie cookie : cookies) {
            if (!cookie.isExpired(now)) {
                if (cookieSpec.match(cookie, cookieOrigin)) {
                    if (this.log.isDebugEnabled()) {
                        this.log.debug("Cookie " + cookie + " match " + cookieOrigin);
                    }
                    matchedCookies.add(cookie);
                }
            } else {
                if (this.log.isDebugEnabled()) {
                    this.log.debug("Cookie " + cookie + " expired");
                }
                expired = true;
            }
        }

这里的for循环遍历了我们通过CookieStore设置的所有Cookie。其中有一个cookieSpec.match操作,当这个操作成功后,就会将我们的cookie设置到Header,直接走下去,发现我们自己的cookies没有了,说明这里的match操作失败了,进去看看为什么match失败:

    // CookieSpecBase.java
    @Override
    public boolean match(final Cookie cookie, final CookieOrigin origin) {
        Args.notNull(cookie, "Cookie");
        Args.notNull(origin, "Cookie origin");
        for (final CookieAttributeHandler handler: getAttribHandlers()) {
            if (!handler.match(cookie, origin)) {
                return false;
            }
        }
        return true;
    }

这段代码是match真正执行的地方,通过一组Handler去match,如果有一个失败就退出,并返回失败,看看是哪个失败了:

最后一直走到BasicDomainHandler.java中:

    @Override
    public boolean match(final Cookie cookie, final CookieOrigin origin) {
        Args.notNull(cookie, "Cookie");
        Args.notNull(origin, "Cookie origin");
        final String host = origin.getHost();
        String domain = cookie.getDomain();
        if (domain == null) {
            return false;
        }
        if (domain.startsWith(".")) {
            domain = domain.substring(1);
        }
        domain = domain.toLowerCase(Locale.ROOT);
        if (host.equals(domain)) {
            return true;
        }
        if (cookie instanceof ClientCookie) {
            if (((ClientCookie) cookie).containsAttribute(ClientCookie.DOMAIN_ATTR)) {
                return domainMatch(domain, host);
            }
        }
        return false;
    }

前面先检查了我们通过cookie.setDomain(".zrj.com")设置的domain信息。

关于cookie的domain:

如果domain设置的是".zrj.com",那么对于"t.zrj.com"、"www.zrj.com"等等host地址,这个cookie都应该生效。

这里我们的host是t.zrj.com,而domain是zrj.com,检查不通过(这里检查不通过就很奇怪了,按理说应该通过),然后接下来使用cookie里面的Attribute去检查domain:

if (((ClientCookie) cookie).containsAttribute(ClientCookie.DOMAIN_ATTR))

这句话就是去检查我们cookie里面有没有通过Attribute设置"domain"信息:

    // BasicClientCookie.java
    @Override
    public boolean containsAttribute(final String name) {
        return this.attribs.containsKey(name);
    }

当然,我是通过setDomain方法设置的,下图中可以看到,attribs为空(我们没有设置这个),最终这里判定我们设置的cookie是不合法的。

[站外图片上传中...(image-c72f7f-1559399169649)]

三、解决问题

    CookieStore cookieStore = new BasicCookieStore();

    BasicClientCookie cookie = new BasicClientCookie(cookie_name, ticket);
//    cookie.setDomain(".zrj.com");   // 示意domain
    cookie.setAttribute("domain",".zrj.com");
    cookie.setPath("/");
    cookieStore.addCookie(cookie);

既然它检查了attribs,那我们通过attribs设置domain试试看,改成cookie.setAttribute("domain",".zrj.com");后,再次测试,发现server provider可以顺利拿到我们发送的cookie。

这里我仍然坚持设置domain为".zrj.com",而不是迎合它的检查方法设置成和host一模一样,因为这个cookie可能会在多个子域名使用。

这里非常奇怪,我们通过setDomain方法和setAttribute设置的domain的值是一样的,但是通过setDomain设置的没有通过检查,而通过setAttribute就通过了,怀疑这里是一个Bug,当然也可能是我自己对Cookie的协议理解有问题,回头看一下Cookie的各种协议确认一下。如果有大佬知道这里的原由,请不吝赐教!

PS:

HttpClient版本(maven):


    
      org.apache.httpcomponents
      httpclient
      4.5.6
    

欢迎光临我的个人博客:https://www.jelliclecat.cn/

你可能感兴趣的:(HttpClient 带Cookie请求的一个Bug)