首页 > 疑难杂症 > 当Google Analytics、Firefox和IIS走到了一起…

当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。

GA的乱码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中塞入中文信息。

举个例子,我要为我的博客投一个宣传广告:

那么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的所有文件。

Everything快速搜索

打开这个文件,里头存放的就是IE持久化的cookie信息。

IE存储持久化Cookie的文件 IE存储持久化Cookie的文件——二进制形式

这个时候,我们再在地址栏通过javascript:alert(document.cookie)我们会发现,IE显示的Cookie值和我们一开始设置的是一样的。

看完了本地的Cookie信息,我们接下来看看IE发送给服务器的Cookie又是什么。

我们用Fiddler来监视整个HTTP通讯过程(这里不用HTTP Watch是因为HTTP Watch会将HTTP消息解码后显示出来,没办法看到原始二进制数据,不方便分析)。

我们再向我的博客首页发起一次访问,在Fiddler中我们会看到:

(文本形式)

Fiddler观察发送中文Cookie(文本形式)

(二进制原始数据)

Fiddler观察发送中文Cookie(二进制形式)

我们很惊奇的看到,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”字符串。

Firefox设置Cookie

但如果你再次通过Javascript:alert(document.cookie)你会发现,这次弹出的内容变了:

Firefox显示乱码的Cookie

我们通过Web Developer Toolbar查看当前域下的Cookie,发现,目前的Cookie确实是像上面第二个对话框所示的,是带乱码的:

Web Developer Bar看到的乱码的Cookie

我们现在关心的问题是,这个乱码是怎么来的?

我们先把这串文字拷贝到Notepad++中(注意,需要将Notepad++调到UCS-2编码状态下)看一下对应的字节是什么。

乱码cookie的二进制

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来构造这样的“非法”请求:

Wfetch发送异常请求 UserAgent中包含非打印字符

服务端一样会抛出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始终访问不了新东方了...)。(update:2010-7-2)或者你在投放广告的时候,Url参数中的广告系列、广告媒介以及广告来源这三个跟踪参数不要包含中文信息(即使是UTF-8编码过的),全部使用英文,这样也不会有问题。

希望整个分析过程对你有所帮助。

——Kevin Yang

本博客遵循CC协议2.5,即署名-非商业性使用-相同方式共享
写作很辛苦,转载请注明作者以及原文链接~
如果你喜欢我的文章,你可以订阅我的博客:-D点击订阅我的文章

  1. nike shox
    | #1

    难道就只能让GA来修改额? .. 还是? 感觉很麻烦的样子

    • Kevin Yang
      | #2

      不投Firefox,或者广告系列、广告媒介以及广告来源不要包含中文(即使是UTF-8编码)就不会有问题。这个实现还是比较简单的。

  2. zhou
    | #3

    出现这种情况,一般是用了GA做手动标记的时候,自动标记不会有此问题,如果非要手动标记,就建议不用写中文。

    • Kevin Yang
      | #4

      自动标记只会添加gclid参数,不会添加其他额外广告参数,所以不会影响。

  3. Iceberg Guan
    | #5

    我认真地学习了此文,收获非常大,谢谢Kevin!

    不过还有几个事情不太清楚:
    1.所谓“GA就会直接往Cookie中塞入中文信息”,这里应该也是会有编码的吧?只不过不一定是utf-8。不知道我理解的对不。
    2.ga.js的代码不知道你是怎么读出来的,我下了一个看,google在脚本里用了迷惑性的手段,基本没法阅读。。。
    3.好像不光是IIS,FF遇到tomcat也可能会有类似问题。

    PS. Yangchengfei?

    • Kevin Yang
      | #6

      1. 我这里所说的GA直接往cookie里头塞中文信息,实际上是针对脚本解析器来说的,脚本解析器处理的都是字符,但是字符在存储的时候肯定是必须通过字符编码转换成二进制的,而使用的编码是取决于不同浏览器和平台的。所以我们才要将非ASCII码的字符转换成ASCII码字符(通过escape或者encodeURI等函数),这样编码才是确定的。
      2. GA代码确实是混淆得很离谱,我很久之前花了好长一段时间才研究完,所以对GA的实现了解才比别人要懂得稍微多一点。
      3. 其他的Web服务器我接触得不多,没有时间去试验。这里只是把我研究出来的东西分享给大家。
      PS: No. 看你也是技术很感兴趣的人,以后常交流~

  4. Iceberg Guan
    | #7

    1.嗯,明白了!
    2.我想说:你真NB! 如果你能来篇《杨氏GA源代码解析》的话,好多人就可以站在你的肩膀上了:)
    3.嗯嗯,我也说如我知道的:)

    PS:sorry啊~看你也是BYR,还以为是同一级的同学呢~~以后会多学习你的博客~
    忽然想起好久没上BYR了……

  5. | #8

    It usefull to get details of ga.js here.thanks.

  1. 暂时没有trackbacks.