一、情景复现
最近在使用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"); 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
| @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
| 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
| 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
| @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
| @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.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
| <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.6</version> </dependency>
|