记录因为 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" : [ ]
        }
    }
}
(你没看错!errInfo 的 no 真的是 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 容器 CookieContainer 和 CookieCollection(前者用于 HttpWebRequest,后者用于调试输出)的项并不一样。
观察:
- 访问 https://www.baidu.com,
CookieContainer4项,CookieCollection7项; - 访问 https://passport.baidu.com,
CookieContainer5项,CookieCollection8项。 - 第2步之后测试输出 
CookieContainer中URI=x.baidu.com(即,找出可以用于 baidu.com 全域的 cookie)的 cookie,结果只剩下一个名称奇怪的东西。 
毫无疑问,CookieContainer 的更新出现了问题。以前我写过未来花园的登录,框架未变,而代码失效了——说明应该是 cookie 被丢弃了。
被谁丢弃了呢?我决定观察浏览器的处理过程。首先从零开始访问百度,观察浏览器的 cookie 流动。(不要在 Firefox 上试,这家伙从空白页开启网络流监控要求重新加载页面,也就是说无法获取“从零开始”的状态信息。)第一次获取了 BAIDUID 和 BIDUPSID 和另外几个东西,刷新的时候二者都不变(处于“发送”栏目中,没有被 set-cookie)。观察这二者的原因是在之后的步骤中,BAIDUID 是很重要的状态信息;BIDUPSID 看名称似乎是有点用的……
接着观察程序中的 cookie 流动。结果发现,第二次请求居然会获得一个新的 BAIDUID,而且第一次明明存在用于域 .baidu.com 的 BIDUPSID,却在第二次请求中消失了!所以可以知道,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日。
再仔细看 CookieContainer 的 Add() 方法的执行结果。可以看到,对于被标记为已经过期(Expires 和 Expired 属性)的 Cookie,CookieContainer 不会添加。
这应该可以被写入千年虫问题的应用范例了吧。我是不是应该给 .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/?login(GET 方式),看到返回的一段东西我当时就笑了。