“Server.UrlDecode(Server.UrlEncode("北京")) == “北京””,先用UrlEncode编码然后用UrlDecode解码,这条语句永远为true吗?答案是否定的,结果可能与很多人预想的不大一样。本文主要分析这一问题出现的原理,研究下Server.UrlEncode(),Server.UrlDecode(),Request["xxx"]三个函数与编码方式的关系。
1. 问题出现的情景
网站采用了GB2312编码,在Web.config中添加如下配置。
<system.web> <globalization requestEncoding="GB2312" responseEncoding="GB2312"/> </system.web>
测试页面EncodeServerTest.aspx.cs代码。
protected void Page_Load(object sender, EventArgs e) { string s = Server.UrlDecode(Server.UrlEncode("北京")); bool isEqual = s == "北京"; }
测试页面EncodeServerTest.aspx代码。
View Code
运行页面,首次执行时,编码解码方式都为GB2312,isEuqal=true;点击页面的button,通过ajax再次请求页面,编码方式仍为GB2312,但解码方式变成了UTF-8,于是s值成了乱码,isEqual=false。下面两个图分别为两次执行的结果:
实际项目遇到问题的场景比这复杂,但也是因为UrlEncode编码和UrlDecode解码方式不一致造成的,本系列的第三篇会有实际项目场景的说明。要解释这一现象,必须了解UrlEncode()和UrlDecode()的实现。
2. Server.UrlEncode()函数
反编译UrlEncode()函数,实现如下:
public string UrlEncode(string s) { Encoding e = (this._context != null) ? this._context.Response.ContentEncoding : Encoding.UTF8; return HttpUtility.UrlEncode(s, e); }
从源码可以看出,有上下文时用的是Response.ContentEncoding,没有上下文时默认用UTF-8编码。关键是Response.ContentEncoding的实现,继续反编译ContentEncoding的实现:
public Encoding ContentEncoding { get { if (this._encoding == null) { GlobalizationSection globalization = RuntimeConfig.GetLKGConfig(this._context).Globalization; if (globalization != null) { this._encoding = globalization.ResponseEncoding; } if (this._encoding == null) { this._encoding = Encoding.Default; } } return this._encoding; } }
结论:UrlEncode()函数,优先从取配置文件的Globalization结点获取,如果配置文件没有的话用Encoding.Default,最后默认用Encoding.UTF8。
3. Server.UrlDecode()函数
反编译UrlEncode()函数,实现如下:
public string UrlDecode(string s) { Encoding e = (this._context != null) ? this._context.Request.ContentEncoding : Encoding.UTF8; return HttpUtility.UrlDecode(s, e); }
从源码可以看出,有上下文时用的是Request.ContentEncoding,没有上下文时默认用UTF-8编码。关键是Request.ContentEncoding的实现,继续反编译ContentEncoding的实现:
public Encoding ContentEncoding { get { if (!this._flags[0x20] || (this._encoding == null)) { this._encoding = this.GetEncodingFromHeaders(); if ((this._encoding is UTF7Encoding) && !AppSettings.AllowUtf7RequestContentEncoding) { this._encoding = null; } if (this._encoding == null) { GlobalizationSection globalization = RuntimeConfig.GetLKGConfig(this._context).Globalization; this._encoding = globalization.RequestEncoding; } this._flags.Set(0x20); } return this._encoding; } set { this._encoding = value; this._flags.Set(0x20); } }
从源码可以看出,Request.ContentEncoding先通过函数GetEncodingFromHeaders()获取,如果获取不到,则从配置文件获取,接下来看GetEncodingFromHeaders()的实现:
View Code
从GetEncodingFromHeaders()的源码可以看出,先从HTTP请求头(x-up-devcap-post-charset或者charset)获取编码信息,如果编码合法的话则采用HTTP请求头指定的编码方式解码。
结论:UrlDecode()函数,优先从HTTP请求头(x-up-devcap-post-charset或者charset)获取编码,如果没指定的话从取配置文件的Globalization结点获取,最后默认Encoding.UTF8。
通过对UrlEncode()和UrlDecode()源码的分析,可以看出两者在确定编码上并不一致,UrlDecode()和HTTP请求的头有关,而通过Fiddler对比EncodeServerTest.aspx页面的两次请求,发现通过Ajax方式的请求,请求头正好多了“Content-Type:application/x-www-form-urlencoded; charset=UTF-8”一句,文章开始的问题得以解释。
补充:获取Response.ContentEncoding和Request.ContentEncoding时,还有一个重要的函数”GlobalizationSection globalization = RuntimeConfig.GetLKGConfig(this._context).Globalization“,网上关于这个函数的资料很少,反编译后代码也很复杂,看的云里雾里,下面摘录一部分代码,从总可以猜测这个函数的功能:根据配置文件的继承关系,取配置文件中Globalization结点的Request和Response编码方式,如果没取到的话默认取UTF-8编码,个人感觉获取Request.ContentEncoding时的分支Encoding.Default赋值应该不会被执行。
View Code
4. Request["xxx"]
Request[key],根据指定的key,依次访问QueryString,Form,Cookies,ServerVariables这4个集合,如果在任意一个集合中找到了,就立即返回。通常如果请求是用GET方法发出的,那我们一般是访问QueryString去获取用户的数据,如果请求是用POST方法提交的, 我们一般使用Form去访问用户提交的表单数据。
public string this[string key] { get { string str = this.QueryString[key]; if (str != null) { return str; } str = this.Form[key]; if (str != null) { return str; } HttpCookie cookie = this.Cookies[key]; if (cookie != null) { return cookie.Value; } str = this.ServerVariables[key]; if (str != null) { return str; } return null; } }
Request.QueryString[key]实现源码如下,从中可以看到经过层层调用,最终调用的是”base.Add(HttpUtility.UrlDecode(str, encoding), HttpUtility.UrlDecode(str2, encoding));“添加到集合中,而是用的解码方式encoding和Server.UrlDecode()函数是一致的,都是Request.ContentEncoding。
View Code
Request.Form[key]实现源码如下,从中可以看到经过层层调用,最终调用的是”HttpUtility.UrlDecode(bytes, num4 + 1, (i - num4) - 1, encoding);“添加到集合中,而调用的解码方式encoding和Server.UrlDecode()函数是一致的,都是Request.ContentEncoding。
View Code
Request.Cookies[key],最终没有调用解码函数,只是把HTTP请求中Cookie值取出来了,如果存储Cookie时,对数据进行了编码处理,通过Request.Cookies[key]获取到Cookie值,需要调用对应的解码函数进行解码。最好调用函数HttpUtility.UrlDecode(str, encoding)解码,以免因为HTTP请求不同造成解码方式不同而出错(对应Server.UrlDecode()函数)。
5. 本文结论
Request.QueryString[key]、Request.Form[key]默认都会调用函数HttpUtility.UrlDecode(str, encoding),如果HTTP请求的数据只经过一次编码,无需再调用解码函数;Request.Cookies[key]没用调用解码函数,获取到值后需要调用正确的解码函数才能得到正确的值。
Request.QueryString[key]、Request.Form[key]、Server.UrlDecode(),解码方式获取是一致的,都是优先从HTTP请求头(x-up-devcap-post-charset或者charset)获取编码,如果没指定的话从取配置文件的Globalization结点获取,最后默认Encoding.UTF8。
Server.UrlEncode()解码方式,优先从取配置文件的Globalization结点获取,如果配置文件没有的话用Encoding.Default,最后默认用Encoding.UTF8。
Server.UrlEncode()和Server.UrlDecode(),获取编码方式并不一样,两者成对使用结果并不一定正确,这个和我们通常的认识不一致,需要特别注意。