记录因为 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,
CookieContainer
4项,CookieCollection
7项; - 访问 https://passport.baidu.com,
CookieContainer
5项,CookieCollection
8项。 - 第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
方式),看到返回的一段东西我当时就笑了。