HttpClient 带Cookie请求的一个Bug


一、情景复现

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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的。


1
CloseableHttpResponse response = httpClient.execute(httpGet);

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

一直到ProtocolExec中:

1
this.httpProcessor.process(request, context);

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

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

1
2
3
4
5
6
7
8
9
// 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中。

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

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

1
2
3
4
5
6
7
8
9
// 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);
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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失败:

1
2
3
4
5
6
7
8
9
10
11
12
// 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中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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:

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

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

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

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

三、解决问题

1
2
3
4
5
6
7
    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):

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
</dependency>
我不喝咖啡,但是我相信知识有价。