当Google Analytics、Firefox和IIS走到了一起...
今天同事在投放AdWords广告的时候发现了一个诡异的现象:
使用Firefox点击AdWords广告跳转到客户网站上之后,再次刷新页面或者浏览其他页面均提示“Bad Request”的HTTP错误(错误码400)。
而IE、Chrome下则没有这个问题。
Cookie惹的祸
由于HTTP本身是无状态的,用来实现状态维持的技术一般都是Cookie。而之前我也遇到过几次因为Cookie导致的访问异常。一次是同事用Firefox死活访问不了新东方网站(参见我之前的文章:Firefox无法访问特定网站),一次是我自己死活登录不了Gmail帐号。这两个问题最终都是以清空Cookie解决的。所以这次有经验了,用web developer bar查看当前客户网站下都有哪些Cookie,一瞄,发现一个带乱码的Cookie。
不用想,也知道这是因为中文没有编码就直接塞到Cookie里头导致的乱码。看看Cookie的来头,__utmz,是Google Analytics(GA)植入的。删除此Cookie之后,访问正常。
Google Analytics的Cookie编码问题
同事测试的那个广告的Url添加了Google Analytics支持的跟踪参数,并且客户网站上也部署了GA的代码。
GA在执行时会检测当前Url中是否包含广告跟踪参数(至少必须包含utm_source),一旦发现,则认为是付费流量,这个时候它就会提取广告信息中的来源(utm_source),广告系列(utm_campaign)和广告媒介(utm_medium),对其进行解码(先尝试用decodeURIComponent函数,失败的话再用unescape函数),最后持久化存储到__utmz这个Cookie中。但是就在写入Cookie这一步,GA漏掉了编码操作。也就是说,如果我们的广告系列或者广告媒介的原始信息包含中文,那么GA就会直接往Cookie中塞入中文信息。
举个例子,我要为我的博客投一个宣传广告:
- 广告系列:Kevin博客宣传
- 广告来源:google
- 广告媒介:PPC
- 带跟踪参数的着陆页面Url:http://www.imkevinyang.com/?utm_source=google&utm_medium=ppc&utm_campaign=Kevin%E5%8D%9A%E5%AE%A2%E5%AE%A3%E4%BC%A0
那么GA在写Cookie的时候,会执行类似下面的代码(当然这里简化了__utmz的值):
var data = "Kevin博客宣传"; // GA错误的Cookie操作 document.cookie = "_utmz=" + data; // 正确的Cookie存储操作 document.cookie = "_utmz=" + encodeURI(data);
使用Javascript对Cookie进行存取,标准的操作应该是在存入的时候编一次码,取出的时候解一次码。这样保证存放在Cookie中的都是ASCII字符。早期JS使用escape/unescape进行编解码,现在通常使用encodeURI或者encodeURIComponent函数,这两个函数用的都是UTF-8编码。
中文Cookie潜在的问题
那么当我们直接将中文直接存到Cookie又会发生了什么事呢?IE和Firefox的行为有什么不一样的地方呢?我们在IE8和Firefox3.6下做几个实验。
IE8对中文Cookie的处理
实验步骤:
- 打开IE8,清空所有Cookie和缓存,建立干净的测试环境。
- 访问http://www.imkevinyang.com/
- 地址栏执行javascript:alert(document.cookie="mycookie=缂栫爜编码;expires=Mon, 25 May 2020 10:31:49 GMT"),写入一个持久化cookie。
这样就在我的博客上设置了一个2020年5月25号过期的cookie了。之所以要设置持久化cookie而不是会话Cookie,是因为IE会将持久化Cookie写入到硬盘上了,这样方便我们了解这个过程,而会话cookie我目前还不清楚他存储的位置。
细心的你会注意到,上面这个cookie的值很奇怪,有几个乱码。其实那段乱码是我把“编码”这两个汉字的UTF-8编码(6个字节)使用GB2312解码(每两个字节对应一个字符)后得到的字符。至于为什么要这样测试,一会我们就会知道了。
IE地址栏用的是ANSI编码,也就是说当你在地址栏输入中文的时候,IE会将中文字符以系统默认字符集进行编码。当你使用中文系统时,地址栏的“编码”字符,实际上最后会被编码为B1 E0 C2 EB四个字节,而在英文系统下,系统使用的是西方字符集作为默认字符集,没有中文字符,因此“编码”这两个字符会被替换成?,也就是3F。
IE在创建cookie文件,会自动选择最合适的编码。当我们写入“缂栫爜编码”(GB2312编码后得到二进制流E7 BC 96 E7 A0 81 B1 E0 C2 EB),由于最后四个字节无法用UTF-8解码,因此IE会将文件存储为GB2312。(如果你只测试“缂栫爜”的话,IE会将文件存储为UTF-8)。
好了,现在让我们来看看文件里头都是什么内容。
打开everything工具,搜索“www.imkevinyang txt”这样就会列出文件名包含www.imkevinyang和txt的所有文件。
打开这个文件,里头存放的就是IE持久化的cookie信息。
这个时候,我们再在地址栏通过javascript:alert(document.cookie)我们会发现,IE显示的Cookie值和我们一开始设置的是一样的。
看完了本地的Cookie信息,我们接下来看看IE发送给服务器的Cookie又是什么。
我们用Fiddler来监视整个HTTP通讯过程(这里不用HTTP Watch是因为HTTP Watch会将HTTP消息解码后显示出来,没办法看到原始二进制数据,不方便分析)。
我们再向我的博客首页发起一次访问,在Fiddler中我们会看到:
(文本形式)
(二进制原始数据)
我们很惊奇的看到,IE发送的并不是我们设置的那些字符“缂栫爜编码”(二进制是E7 BC 96 E7 A0 81 B1 E0 C2 EB),而是“编码����”(现在知道我为什么要用“缂栫爜编码”做测试了把)。对应的二进制是E7 BC 96 E7 A0 81 EF BF BD EF BF BD EF BF BD EF BF BD。注意到,IE将原始信息的后面4个字节替换成了EF BF BD.
这是因为IE发送HTTP消息的时候会检测字节流是否是能够以UTF-8解码,如果不行,那么会将相应的异常字节替换成EF BF BD(也就是对应�字符)。这有点类似于我们之前提到的,英文系统对于缺失的字符会使用?号替代。
Firefox对于中文Cookie的处理
Firefox不像IE那样把Cookie直接存储为文件的形式,所以我们研究起来没那么方便。
不过我们还是按照上面同样的步骤来做实验,不过这次为了简单起见我们修改一下测试的Cookie值。
- 打开Firefox,清空所有Cookie和缓存,建立干净的测试环境。
- 访问http://www.imkevinyang.com/
- 地址栏执行javascript:alert(document.cookie="mycookie=1编码1")
-
第一次Firefox弹出的对话框显示我们Cookie应该是设置成功了,返回“1编码1”字符串。
但如果你再次通过Javascript:alert(document.cookie)你会发现,这次弹出的内容变了:
我们通过Web Developer Toolbar查看当前域下的Cookie,发现,目前的Cookie确实是像上面第二个对话框所示的,是带乱码的:
我们现在关心的问题是,这个乱码是怎么来的?
我们先把这串文字拷贝到Notepad++中(注意,需要将Notepad++调到UCS-2编码状态下)看一下对应的字节是什么。
31是字符“1”的ASCII码。而16和01是哪来的呢?
其实是Unicode Code Point。“编码”的Unicode码是“7F16 7801”。上面显示的16和01就是截断了Unicode码高位得到的。为了证实这个结论,我又测试了好几个中文cookie,均是如此。
也就是说,Firefox的地址栏使用的是Unicode码,也就是说当你输入“mycookie=1编码1”这样的字符串的时候,Firefox看到的是:
\u006d\u0079\u0063\u006f\u006f\u006b\u0069\u0065\u003d\u0031\u7f16\u7801\u0031
在存储中文Cookie的时候,他会将Unicode的高位截断,保留低位。然后写入Cookie存储。这也是为什么我们会看到“编码”这个Cookie变成了“16 01”。
Firefox向服务端发送HTTP请求时对于http消息的编码处理方式和IE的一样,也是判断字节流能够以UTF-8进行解码,这里就不再赘述了。有兴趣的朋友可以按照上面的方法去测试。
为什么Firefox无法访问
基于上面对IE和Firefox对中文Cookie的处理方式的了解,我们现在可以知道,对于中文Cookie,IE是用ANSI编码,也就是说Cookie中永远不会出现ASCII字符集中的不可打印字符(GB2312编码每个字节也都是从A0开始的),而Firefox采用Unicode码,却又对其进行了高位截断,导致Cookie有可能会出现ASCII字符集中的非打印字符。
IE和Firefox在构造HTTP消息的时候对于字节流序列编码问题的处理方式一样。无法使用UTF-8解码的字节流序列,将其替换成EF BF BD,这个我们在Fiddler中已经看到了。而对于ASCII字符集的非打印字符则不做任何处理,直接发送到服务器端。
所以用Firefox访问,服务端收到的HTTP Request有可能包含非打印字符,而IE访问的话,则不会出现这样的情况。
例如Firefox上设置了一个中文Cookie,“我”,Unicode码是62 11,被Firefox高位截断了,就剩下11了,对应着ASCII码表中的Device Control 1,也就是控制字符。那么当你带着这个Cookie向服务端发起请求的时候服务端有可能就会直接抛出Bad Request的异常,告诉客户端,你发过来的请求不符合HTTP规范。
所以实际上不只是Cookie不能出现这样的非打印字符,其他HTTP Header中也不能出现这样的非打印字符。我们可以直接使用WFetch来构造这样的“非法”请求:
服务端一样会抛出400 Bad Request。
IIS和Apache的不同处理方式
当客户端发起的请求存在问题时,服务端的处理方式是取决于不同服务器的实现的。我们上面讨论的这个问题,实际上只会对IIS造成影响,对那些后台采用Apache或者LiteSpeed这类的服务器不会有影响。这说明IIS的容错性还是稍微差一点,不知道从安全的角度来考虑是好事还是坏事。
总结回顾
上面讲了那么多,你可能听着有点乱了。我们重新来整理一遍整个故事。
广告代理商投了一个广告,着陆页面Url中添加了google的广告参数,其中带有中文信息,客户网站上部署了GA代码,GA读取到此中文信息之后直接扔到Cookie中而没有经过编码。Firefox内部将此中文的Unicode码高位截断保留低位存下来。当你再次刷新页面的时候,Firefox把这个截断的字符发给IIS服务器,而刚好这个截断之后的字符是一个非打印字符,IIS觉得自己无法处理,就抛出一个Bad Request,告诉客户端此请求非法,我无法处理。
整个故事就这样。
怎么办呢?建议为了保险起见,如果客户网站服务器用的是IIS,那么你还是不要在Firefox上投放那些Url带中文(即使是UTF-8编码过)的广告了,否则可能浪费钱,因为用户来了,再点一次可能就无法访问了,而且以后可能都无法访问了(现在终于知道为什么我那同事当时用Firefox始终访问不了新东方了...)。
希望整个分析过程对你有所帮助。