FineUI(开源版)v6.0中FState服务器端验证的实现原理
前言
1. FineUI(开源版)是完整开源,最早发起于 2008-04,下载全部源代码:http://fineui.codeplex.com/
2. 你可以通过捐赠作者来支持FineUI(开源版)的发展:http://fineui.com/donate/
FineUI的FState与ViewState
早在2013-01 我曾写过一篇文章,对FState有详细介绍:http://www.cnblogs.com/sanshi/archive/2013/01/08/2850459.html
现在来简要回顾一下:
1. ViewState是ASP.NET WebForm的基石,用来在页面回发过程中维持控件状态,这样我们才能在后台方便的使用控件的服务器端属性。
2. FineUI的AJAX回发过程中,相同的数据会同时存在于ViewState和返回的JavaScript代码中,造成数据重复浪费!
3. FState机制替换ViewState后,只会在回发数据中保留一份数据,减少了数据的传输量。
对于,常见的误解与纠正:
1. FineUI中不能使用ViewState了。错!!
FineUI只是实现了一套类似ViewState的机制,但是ViewState本身还是存在的,你依然可以在页面上调用ViewState对象存储数据。
2. 不使用ViewState了,FineUI控件不能维持状态了。错!!
FState是在AJAX环境中对ViewState的一种改进和提高,目的是为了减少数据传输量。你依然可以方便在C#代码中使用控件属性
FineUI中的FState可以被恶意篡改
FState用来在页面回发过程中维持控件的状态,但是由于FState完全以JavaScript变量的形式暴露出来,很容易被恶意用户在客户端进行篡改。
首先来看一个简单的页面:
<f:PageManager ID="PageManager1" runat="server" /> <f:DropDownList runat="server" ID="DropDownList1"> <f:ListItem Text="可选项1" Value="Value1" Selected="true" /> <f:ListItem Text="可选项2" Value="Value2" /> <f:ListItem Text="可选项3" Value="Value3" /> </f:DropDownList> <f:Button runat="server" Text="提交" ID="btnSubmit" OnClick="btnSubmit_Click"></f:Button>
后台的按钮事件:
protected void btnSubmit_Click(object sender, EventArgs e) { Alert.Show("下拉列表选中项:" + DropDownList1.SelectedValue); }
页面运行效果:
在页面生成的HTML代码,我们可以看到 f_state 的身影:
下面我们通过一个例子来讲解 FState 的作用,假如用户在前台对下拉列表的数据进行了重新绑定:
var ddl = F("DropDownList1"); var newdata = [ ["Data1", "数据1", 1], ["Data2", "数据2", 1], ["Data3", "数据3", 1] ]; ddl.store.loadData(newdata); ddl.setValue("Data1");
此时点击提交按钮,效果:
之所以后台取不到下拉列表的选中值,是因为后台从FState恢复了下拉列表的项分别是“选项一”,“选项二”和“选项三”。
而对于客户端重新绑定的新数据源,后台一无所知,因此拿新的选中项值 Data1 去检索时,自然就找不到对应的项了,所以此时SelectedValue==null
这个逻辑自然是正确的,但是由于 FState 是以JavaScript的形式返回到页面的,所以恶意用户自然就可以篡改这个值了:
var ddl = F("DropDownList1"); var newdata = [ ["Data1", "数据1", 1], ["Data2", "数据2", 1], ["Data3", "数据3", 1] ]; ddl.f_state.F_Items = newdata; ddl.store.loadData(newdata); ddl.setValue("Data1");
此时再点击提交按钮:
此时服务器已经接受了这个客户端恶意篡改的值!!这个就不对了。
如果是文本输入框的值,我们自然是要手工进行服务器端验证的,不要相信客户端传入的任何值,因为都有可能被篡改!
但是如果能默认提供一种内置的验证机制,让这种恶意修改FState的行为消失,岂不是更好。FineUI v6.0对此进行了增强。
FineUI v6.0 中默认的FState服务器端验证
完全相同的例子,在 FineUI v6.0 中,如果通过客户端修改下拉列表的f_state和内部数据,此时提交按钮:
这个就是我们的保护机制,保护服务器端输出的FState信息不会在客户端被恶意修改。
那么这种保护机制是如何实现的呢?我们从生成的网页代码来分析一下:
可以看到,控件除了生成 f_state 属性,还额外附加了一个 f_state_v 属性,这个很容易理解为对 f_state 的加密值。
那么在页面回发时,只需要把这个 f_state_v 的值一块回发,后台进行解密验证即可。我们来看下HTTP请求的参数:
这里没有 f_state_v 的身影,那是因为他隐藏在 F_STATE 变量中的,这个值是 Base64 编码的,我们解码后看下:
{ "DropDownList1": [{ "F_Items": [ ["Data1", "\u6570\u636e1", 1], ["Data2", "\u6570\u636e2", 1], ["Data3", "\u6570\u636e3", 1] ], "SelectedValue": "Value1", "SelectedValueArray": ["Value1"] }, "e1ae24"] }
可以看到,这个 f_state_v 的确一起回发到后台了。
这个逻辑其实并不复杂:
1. 页面初始化时,除了生成控件的 f_state 之外,还额外的生成一个加密后的信息 f_state_v
2. 页面回发时,后台把这两个值进行校验,就知道是否在客户端被修改了
3. 如果后台控件的属性发生变化,还重新生成 f_state_v 更新到前台
这里给出后台的主要逻辑代码,完整源代码请自行下载:
private static string GetFStateValidation(JObject stateObj) { string fstate = stateObj.ToString(Newtonsoft.Json.Formatting.None); return GetShortMD5HashWithSeed(fstate); } private static bool ValidateFState(JObject stateObj, string validationString) { string fstate = stateObj.ToString(Newtonsoft.Json.Formatting.None); string fstateCode = GetShortMD5HashWithSeed(fstate); if (fstateCode == validationString) { return true; } else { return false; } } private static string GetShortMD5HashWithSeed(string fstate) { string md5HashStr = StringToMD5Hash(fstate + GetFStateValidationSeed()); return md5HashStr.Substring(0, 3) + md5HashStr.Substring(md5HashStr.Length - 3, 3); } private static string StringToMD5Hash(string inputString) { MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider(); byte[] encryptedBytes = md5.ComputeHash(Encoding.ASCII.GetBytes(inputString)); string a = System.Text.Encoding.Default.GetString(encryptedBytes); StringBuilder sb = new StringBuilder(); for (int i = 0; i < encryptedBytes.Length; i++) { sb.AppendFormat("{0:x2}", encryptedBytes[i]); } return sb.ToString(); } private static string _fstateValidationSeed = String.Empty; private static string GetFStateValidationSeed() { if (String.IsNullOrEmpty(_fstateValidationSeed)) { _fstateValidationSeed = new Guid().ToString(); } return _fstateValidationSeed; }
小结
这篇文章讲解了FineUI中的FState取代ViewState的原因,恶意用户如何在客户端篡改FState,然后介绍了FineUI v6.0对 FState 的保护机制。
然后我们从生成的页面HTML入手,简要分析了FState验证机制的实现原理。
感兴趣的朋友可以自行下载源代码分析:http://fineui.codeplex.com/
关于开源和坚持
FineUI(开源版)开始于 2008-04,8年多时间内,我们坚持更新了 128 个版本,内部使用的 extjs 从最初的 v2.x,v3.x,到后来的v4.x,FineUI v6.0 使用了最新的extjs v6.2.0。
8 年间,我们看过太多的开源项目轰轰烈烈的来,平平淡淡的去,那些曾经熟悉的身影,曾经陪伴我们的代码,都已经不复存在。其实很多时候,开源项目不是被新技术淘汰,而是被开源作者所丢弃,不免让人扼腕叹息。
每个存在都有存在的价值,时间总会让之前的东西看起来不再那么新奇好玩,但是还有那么一帮曾经关注的网友,一直在使用的用户,只有不断的更新,才不会让关心你的人失望。
任何事物的存在价值是无限的!