资讯专栏INFORMATION COLUMN

HttpClient获取Cookie的一次踩坑实录

Richard_Gao / 514人阅读

摘要:大多数情况下,我们使用内置的策略,便能够方便直接地获取这些。在默认的实现中,主要的工作是判断中规范的类型,然后再调用具体的某一个实现。

本文原地址:http://www.fullstackyang.com/...,转发请注明本博客地址或segmentfault地址,谢谢!

在使用HttpClient进行抓取一些网页的时候,经常会保留从服务器端发回的Cookie信息,以便发起其他需要这些Cookie的请求。大多数情况下,我们使用内置的cookie策略,便能够方便直接地获取这些cookie。
下面的一小段代码,就是访问http://www.baidu.com,并获取对应的cookie:

@Test
public void getCookie(){
    CloseableHttpClient httpClient = HttpClients.createDefault();
    HttpGet get=new HttpGet("http://www.baidu.com");
    HttpClientContext context = HttpClientContext.create();
    try {
        CloseableHttpResponse response = httpClient.execute(get, context);
        try{
            System.out.println(">>>>>>headers:");
            Arrays.stream(response.getAllHeaders()).forEach(System.out::println);
            System.out.println(">>>>>>cookies:");
            context.getCookieStore().getCookies().forEach(System.out::println);
        }
        finally {
            response.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }finally {
        try {
            httpClient.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

打印结果

>>>>>>headers:
Server: bfe/1.0.8.18
Date: Tue, 12 Sep 2017 06:19:06 GMT
Content-Type: text/html
Last-Modified: Mon, 23 Jan 2017 13:28:24 GMT
Transfer-Encoding: chunked
Connection: Keep-Alive
Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
Pragma: no-cache
Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/
>>>>>>cookies:
[version: 0][name: BDORZ][value: 27315][domain: baidu.com][path: /][expiry: null]

但是也有一些网站返回的cookie并不一定完全符合规范,例如下面这个例子,从打印出的header中可以看到,这个cookie中的Expires属性是时间戳形式,并不符合标准的时间格式,因此,httpclient对于cookie的处理失效,最终无法获取到cookie,并且发出了一条警告信息:“Invalid ‘expires’ attribute: 1505204523”

警告: Invalid cookie header: "Set-Cookie: yd_cookie=90236a64-8650-494b332a285dbd886e5981965fc4a93f023d; Expires=1505204523; Path=/; HttpOnly". Invalid "expires" attribute: 1505204523
>>>>>>headers:
Date: Tue, 12 Sep 2017 06:22:03 GMT
Content-Type: text/html
Connection: keep-alive
Set-Cookie: yd_cookie=90236a64-8650-494b332a285dbd886e5981965fc4a93f023d; Expires=1505204523; Path=/; HttpOnly
Cache-Control: no-cache, no-store
Server: WAF/2.4-12.1
>>>>>>cookies:

虽然我们可以利用header的数据,重新构造一个cookie出来,也有很多人确实也是这么做的,但这种方法不够优雅,那么如何解决这个问题?网上相关的资料又很少,所以就只能先从官方文档入手。在官方文档3.4小节custom cookie policy中讲到允许自定义的cookie策略,自定义的方法是实现CookieSpec接口,并通过CookieSpecProvider来完成在httpclient中的初始化和注册策略实例的工作。好了,关键的线索在于CookieSpec接口,我们来看一下它的源码:

public interface CookieSpec {
……
    /**
      * Parse the {@code "Set-Cookie"} Header into an array of Cookies.
      *
      * 

This method will not perform the validation of the resultant * {@link Cookie}s

* * @see #validate * * @param header the {@code Set-Cookie} received from the server * @param origin details of the cookie origin * @return an array of {@code Cookie}s parsed from the header * @throws MalformedCookieException if an exception occurs during parsing */ List parse(Header header, CookieOrigin origin) throws MalformedCookieException; …… }

在源码中我们发现了一个parse方法,看注释就知道正是这个方法,将Set-Cookie的header信息解析为Cookie对象,自然地再了解一下在httplcient中的默认实现DefaultCookieSpec,限于篇幅,源码就不贴了。在默认的实现中,DefaultCookieSpec主要的工作是判断header中Cookie规范的类型,然后再调用具体的某一个实现。像上述这种Cookie,最终是交由NetscapeDraftSpec的实例来做解析,而在NetscapeDraftSpec的源码中,定义了默认的expires时间格式为“EEE, dd-MMM-yy HH:mm:ss z”

public class NetscapeDraftSpec extends CookieSpecBase {

    protected static final String EXPIRES_PATTERN = "EEE, dd-MMM-yy HH:mm:ss z";

    /** Default constructor */
    public NetscapeDraftSpec(final String[] datepatterns) {
        super(new BasicPathHandler(),
                new NetscapeDomainHandler(),
                new BasicSecureHandler(),
                new BasicCommentHandler(),
                new BasicExpiresHandler(
                        datepatterns != null ? datepatterns.clone() : new String[]{EXPIRES_PATTERN}));
    }

    NetscapeDraftSpec(final CommonCookieAttributeHandler... handlers) {
        super(handlers);
    }

    public NetscapeDraftSpec() {
        this((String[]) null);
    }
……
}

到这里已经比较清楚了,我们只需要将Cookie中expires的时间转换为正确的格式,然后再送入默认的解析器就可以了。

解决方法:

自定义一个CookieSpec类,继承DefaultCookieSpec

重写parser方法

将Cookie中的expires转换为正确的时间格式

调用默认的解析方法

实现如下(URL就不公开了,已经隐去)

public class TestHttpClient {
    
    String url = sth;

    class MyCookieSpec extends DefaultCookieSpec {
        @Override
        public List parse(Header header, CookieOrigin cookieOrigin) throws MalformedCookieException {
            String value = header.getValue();
            String prefix = "Expires=";
            if (value.contains(prefix)) {
                String expires = value.substring(value.indexOf(prefix) + prefix.length());
                expires = expires.substring(0, expires.indexOf(";"));
                String date = DateUtils.formatDate(new Date(Long.parseLong(expires) * 1000L),"EEE, dd-MMM-yy HH:mm:ss z");
                value = value.replaceAll(prefix + "d{10};", prefix + date + ";");
            }
            header = new BasicHeader(header.getName(), value);
            return super.parse(header, cookieOrigin);
        }
    }

    @Test
    public void getCookie() {

        CloseableHttpClient httpClient = HttpClients.createDefault();

        Registry cookieSpecProviderRegistry = RegistryBuilder.create()
                .register("myCookieSpec", context -> new MyCookieSpec()).build();//注册自定义CookieSpec

        HttpClientContext context = HttpClientContext.create();
        context.setCookieSpecRegistry(cookieSpecProviderRegistry);

        HttpGet get = new HttpGet(url);
        get.setConfig(RequestConfig.custom().setCookieSpec("myCookieSpec").build());

        try {
            CloseableHttpResponse response = httpClient.execute(get, context);
            try{
                System.out.println(">>>>>>headers:");
                Arrays.stream(response.getAllHeaders()).forEach(System.out::println);
                System.out.println(">>>>>>cookies:");
                context.getCookieStore().getCookies().forEach(System.out::println);
            }
            finally {
                response.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                httpClient.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

再次运行,顺利地打印出正确的结果,完美!

>>>>>>headers:
Date: Tue, 12 Sep 2017 07:24:10 GMT
Content-Type: text/html
Connection: keep-alive
Set-Cookie: yd_cookie=9f521fc5-0248-4ab3ee650ca50b1c7abb1cd2526b830e620f; Expires=1505208250; Path=/; HttpOnly
Cache-Control: no-cache, no-store
Server: WAF/2.4-12.1
>>>>>>cookies:
[version: 0][name: yd_cookie][value: 9f521fc5-0248-4ab3ee650ca50b1c7abb1cd2526b830e620f][domain: www.sth.com][path: /][expiry: Tue Sep 12 17:24:10 CST 2017]

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/70424.html

相关文章

  • 踩坑记录】记一次MySQL主从复制延迟的坑

    摘要:最近开发中遇到的一个主从延迟的坑,记录并总结,避免再次犯同样的错误。运行时查询为空,执行完毕后查询时内容存在,初步怀疑是主从延迟问题。报错只是部分失败,确定是主从延迟的问题。接下来,会去学习主从复制的原理,敬请期待。 最近开发中遇到的一个MySQL主从延迟的坑,记录并总结,避免再次犯同样的错误。 情景 一个活动信息需要审批,审批之后才能生效。因为之后活动要编辑,编辑后也可能触发审批,审...

    cartoon 评论0 收藏0
  • 踩坑实录】Hibernate报错node to traverse cannot be null排查

    最近在改一份二手代码的时候,项目运行报了个java.lang.IllegalArgumentException: node to traverse cannot be null异常。WTF?!难道我HQL写错了?!我只是添加了一个update方法而已啊! 问题排查: showImg(https://segmentfault.com/img/bVbeBmn?w=1657&h=155);这里使用的是J...

    hsluoyz 评论0 收藏0
  • 使用apache的HttpClient进行http通讯,隐藏的HTTP请求头部字段是如何自动被添加的

    摘要:通常情况下,第一次请求完毕后,服务器都会给客户端返回一些字段,在第二次请求时,如果使用的是测试工具或者的这个库,字段都会自动被附加在第二次请求的头部。从里取出前一次请求中由服务器返回的这里把里的加到第二个请求的头部字段,谜底就这样解开了。 我们用apache的HttpClient这个库消费云端的Restful API时,一般都需要两次HTTP调用,第一次获得某种token,比如获取防止...

    meislzhua 评论0 收藏0
  • 使用apache的HttpClient进行http通讯,隐藏的HTTP请求头部字段是如何自动被添加的

    摘要:通常情况下,第一次请求完毕后,服务器都会给客户端返回一些字段,在第二次请求时,如果使用的是测试工具或者的这个库,字段都会自动被附加在第二次请求的头部。从里取出前一次请求中由服务器返回的这里把里的加到第二个请求的头部字段,谜底就这样解开了。 我们用apache的HttpClient这个库消费云端的Restful API时,一般都需要两次HTTP调用,第一次获得某种token,比如获取防止...

    anquan 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<