因 cookie 的 expires 导致的坑爹事

记录因为 cookie 中 expires 设置项类似千年虫的问题导致的坑爹事。

在朝着下一个程序目标进发的时候遇到一个问题。我需要用 HttpWebRequest 去登录百度,在抓包、分析网页以及参看了两位的方法之后写了自己的代码,但是未成功。直接贴两位大神的代码,也未成功。

于是调试。发现每次加载的时候,在获取 token 的时候,总会获得这样一个东西:

{
    "errInfo" : {
        "no" : "0"
        },
    "data": {
        "rememberedUserName" : "",
        "codeString" : "",
        "token" : "the fisrt two args should be string type:0,1!",
        "cookie" : "0",
        "usernametype" : "",
        "spLogin" : "rate",
        "disable" : "",
        "loginrecord" : {
            "email" : [ ],
            "phone" : [ ]
        }
    }
}

(你没看错!errInfono 真的是 0!难道是哪位实习生为了省事而将 token 作为错误信息的传递者而不是用正确的 errInfo 去报告吗?)

原来的 JSON 还有其他问题,如用单引号(标准 JSON 只支持双引号;单引号仅可以用于 JavaScript 的 JSONP)、空格时有时无等等,这里都修正了。

标准的返回应该是这个样子的:

{
    "errInfo" : {
        "no": "0"
        },
    "data" : {
        "rememberedUserName" : "",
        "codeString" : "",
        "token" : "55639c700ece12a35bddf8562b4223fae",
        "cookie" : "1",
        "usernametype" : "",
        "spLogin" : "rate",
        "disable" : "",
        "loginrecord" : {
            "email" : [ ],
            "phone" : [ ]
        }
    }
}

二位大神的示例代码应该是没有问题的,那么可能就是三者共有的问题了。单步调试发现,我维持的两个 cookie 容器 CookieContainerCookieCollection(前者用于 HttpWebRequest,后者用于调试输出)的项并不一样。

观察:

  1. 访问 https://www.baidu.comCookieContainer 4项,CookieCollection 7项;
  2. 访问 https://passport.baidu.comCookieContainer 5项,CookieCollection 8项。
  3. 第2步之后测试输出 CookieContainerURI=x.baidu.com(即,找出可以用于 baidu.com 全域的 cookie)的 cookie,结果只剩下一个名称奇怪的东西。

毫无疑问,CookieContainer 的更新出现了问题。以前我写过未来花园的登录,框架未变,而代码失效了——说明应该是 cookie 被丢弃了。

被谁丢弃了呢?我决定观察浏览器的处理过程。首先从零开始访问百度,观察浏览器的 cookie 流动。(不要在 Firefox 上试,这家伙从空白页开启网络流监控要求重新加载页面,也就是说无法获取“从零开始”的状态信息。)第一次获取了 BAIDUIDBIDUPSID 和另外几个东西,刷新的时候二者都不变(处于“发送”栏目中,没有被 set-cookie)。观察这二者的原因是在之后的步骤中,BAIDUID 是很重要的状态信息;BIDUPSID 看名称似乎是有点用的……

接着观察程序中的 cookie 流动。结果发现,第二次请求居然会获得一个新的 BAIDUID,而且第一次明明存在用于域 .baidu.comBIDUPSID,却在第二次请求中消失了!所以可以知道,BAIDUID 是“被抛弃(discard)的cookie”的代表,应该对其进行研究。

cookie 在什么时候会被丢弃呢?在超时之后。如果不指定超时,则是本次连接有效;指定之前的时间,表示立即丢弃;指定之后的时间,则延迟到未来。当然,受到32位问题的影响,最多到2038年——不过这已经是很长的时间了,从目前来看。

查看 cookie 指定的超时信息。BAIDUID 的过期时间是 expires=Thu, 31-Dec-37 23:55:55 GMT(为了避免2038年问题),这个时间在各大浏览器中被解析为2037年12月31日,但是在 .NET Framework 4.5 下,被解析为1937年12月37日

再仔细看 CookieContainerAdd() 方法的执行结果。可以看到,对于被标记为已经过期(ExpiresExpired 属性)的 CookieCookieContainer 不会添加。

这应该可以被写入千年虫问题的应用范例了吧。我是不是应该给 .NET 那边报一个 issue 呢?(因为 CookieContainer 与现代浏览器在此问题上的行为不一致。)

只好写了一个简略的解决方案:

foreach (Cookie cookie in response.Cookies)
{
    if (cookie.Expires < DateTime.Now.Subtract(TimeSpan.FromDays(365)))
    {
        cookie.Expires += TimeSpan.FromDays(100 * 365);
        cookie.Expired = false;
    }
}

因为设置为强行丢弃的,过期时间一般就设置到一年以前。虽说不是一个美观的方案,但是确实能用了。


随便翻了一下 https://passport.baidu.com/v2/api/?loginGET 方式),看到返回的一段东西我当时就笑了。

分享到 评论