ViewState 到底是什么?ViewState 用于维护页面的 UI 状态。Web 是没有状态的,ASP.NET 页面也没有状态,它们在到服务器的每个往返过程中被实例化、执行、呈现和处理。在 ASP.NET 之前,通过多次回传将值恢复到窗体字段中完全是页面开发人员的责任,他们将不得不从 HTTP 窗体中逐个拾取回传值,然后再将其推回字段中。幸运的是,现在 ASP.NET 可以自动完成这项任务,从而为开发人员免除了一项令人厌烦的工作,同时也无需再为窗体编写大量的代码。但这并不是 ViewState。

 

    ViewState是一种机制,ASP.NET 使用这种机制来跟踪服务器控件状态值,否则这些值将不作为 HTTP 窗体的一部分而回传。例如,由 Label 控件显示的文本默认情况下就保存在 ViewState 中。作为开发人员,您可以绑定数据,或在首次加载该页面时仅对 Label 编程设置一次,在后续的回传中,该标签文本将自动从 ViewState 中重新填充。因此,除了可以减少繁琐的工作和代码外,ViewState 通常还可以减少数据库的往返次数。

 

    ViewState 的工作原理

    ViewState 确实没有什么神秘之处,它是由 ASP.NET 页面框架管理的一个隐藏的窗体字段。当 ASP.NET 执行某个页面时,该页面上的 ViewState 值和所有控件将被收集并格式化成一个编码字符串,然后被分配给隐藏窗体字段的值属性(即 <input type=hidden>)。由于隐藏窗体字段是发送到客户端的页面的一部分,所以 ViewState 值被临时存储在客户端的浏览器中。如果客户端选择将该页面回传给服务器,则 ViewState 字符串也将被回传。

 

    关于 ViewState 还有三个值得注意的小问题

 1、如果要使用 ViewState,则在 ASPX 页面中必须有一个服务器端窗体标记 (<form runat=server>)。窗体字段是必需的,这样包含 ViewState 信息的隐藏字段才能回传给服务器。而且,该窗体还必须是服务器端的窗体,这样在服务器上执行该页面时,ASP.NET 页面框架才能添加隐藏的字段。

 

 2、页面本身将 20 字节左右的信息保存在 ViewState 中,用于在回传时将 PostBack 数据和 ViewState 值分发给正确的控件。因此,即使该页面或应用程序禁用了 ViewState,仍可以在 ViewState 中看到少量的剩余字节。

 

 3、在页面不回传的情况下,可以通过省略服务器端的 <form> 标记来去除页面中的 ViewState。

 

 

 

    设定ViewState的值

 

 

 ViewState中部分值是可以通过代码的形式显示设置或调用的,使用的编程语法和高速缓存的语法类似。

 

 

 

//保存在ViewState中

     ViewState["yourName"] = "Leopold";

 //从 ViewState 中读取

     string yourName = (string)ViewState["SortOrder"];

 

 

 

   ViewState安全吗?

 

 

 

   由于 ViewState 没有被格式化为清晰的文本,某些人有时会认为它被加密了,其实并没有。相反,ViewState 只是进行了 Base64 编码,以确保值在往返过程中不会发生变化,而并不考虑应用程序使用的响应/请求编码。

 

   ViewState在Render事件前在Page.SavePageStateToPersistenceMedium方法中保存,

 

 

 

 //ViewState Saved in Session State

protected override object LoadPageStateFromPersistenceMedium() {

    return Session["ViewState"];

}

 

protected override void SavePageStateToPersistenceMedium(object viewState) {

    Session["ViewState"] = viewState;

    // Bug requires Hidden Form Field __VIEWSTATE

    RegisterHiddenField("__VIEWSTATE", "");

}

 

 

 

   

 

   并在PostBack时通过Page.LoadPageStateFromPerSistenceMedium方法恢复。

 

 

 

protected override object LoadPageStateFromPersistenceMedium() {

    LosFormatter format = new LosFormatter();

    return format.Deserialize(YourDataStore["ViewState"]);

}

 

protected override void SavePageStateToPersistenceMedium(object viewState) {

    LosFormatter format = new LosFormatter();

    StringWriter writer = new StringWriter();

    format.Serialize(writer, viewState);

    YourDataStore["ViewState"] = writer.ToString();

}

 

 

 

    

 

    通过继承两个方法可以很容易地将ViewState保存到Session中,这在低带宽网络环境下可以减少网络流量。现在完全出于兴趣地来看下ViewState的内在结构。每个ViewState在Triplet(System.Web.UI.Triplet)中保存,其中第一个对象是System.Web.UI.Pair,第二个对象是一个以树目录形式保存子控件的数组,第三个对象是以数组保存的与那些子控件相关的Triplets。这些都很难用语言来解释清楚,还是看下例子吧。

 

 //Encoded ViewState:

dDwxMjM0NTY3ODkwO3Q8cDxsPHBycEE7cHJwQjtwcnBDOz47bDx2YWxBO3ZhbEI7dmFsQzs+PjtsPGk8

MD47aTwyPjtpPDM+O2k8NT47PjtsPHQ8cDxsPHBycEE7cHJwQjs+O2w8dmFsQTt2YWxCOz4+Ozs+O3Q8

cDxsPHBycEE7cHJwQjs+O2w8dmFsQTt2YWxCOz4+Ozs+O3Q8cDxsPHBycEE7cHJwQjs+O2w8dmFsQTt2

YWxCOz4+Ozs+O3Q8cDxsPHBycEE7cHJwQjs+O2w8dmFsQTt2YWxCOz4+Ozs+Oz4+Oz4=

 

 

 

 //Decoded ViewState:

t<1234567890;t<p<l<prpA;prpB;prpC;>;l<valA;valB;valC;>>;

l<i<0>;i<2>;i<3>;i<5>;>;l<

t<p<l<prpA;prpB;>;l<valA;valB;>>;;>;

t<p<l<prpA;prpB;>;l<valA;valB;>>;;>;

t<p<l<prpA;prpB;>;l<valA;valB;>>;;>;

t<p<l<prpA;prpB;>;l<valA;valB;>>;;>;>>;>

 

   解析用代码:

 

protected override void SavePageStateToPersistenceMedium(object viewState) {

    // Call Base Method to Not Change Normal Process

    base.SavePageStateToPersistenceMedium(viewState);

 

    // Retrieve ViewState and Write Out to Page

    LosFormatter format = new LosFormatter();

    StringWriter writer = new StringWriter();

    format.Serialize(writer, viewState);

    string vsRaw = writer.ToString();

    Response.Write("ViewState Raw: " + Server.HtmlEncode(vsRaw));

 

    // Decode ViewState and Write Out to Page

    byte[] buffer = Convert.FromBase64String(vsRaw);

    string vsText = Encoding.ASCII.GetString(buffer);

    Response.Write("ViewState Text: " + Server.HtmlEncode(vsText));

 

    // Parse ViewState -- Turn On Page Tracing

    ParseViewState(viewState, 0);

}

 

 

 

 

 

private void ParseViewState(object vs, int level) {

    if (vs == null) {

        Trace.Warn(level.ToString(), Spaces(level) + "null");

    }

    else if (vs.GetType() == typeof(System.Web.UI.Triplet)) {

        Trace.Warn(level.ToString(), Spaces(level) + "Triplet");

        ParseViewState((Triplet) vs, level);

    }

    else if (vs.GetType() == typeof(System.Web.UI.Pair)) {

        Trace.Warn(level.ToString(), Spaces(level) + "Pair");

        ParseViewState((Pair) vs, level);

    }

    else if (vs.GetType() == typeof(System.Collections.ArrayList)) {

        Trace.Warn(level.ToString(), Spaces(level) + "ArrayList");

        ParseViewState((IEnumerable) vs, level);

    }

    else if (vs.GetType().IsArray) {

        Trace.Warn(level.ToString(), Spaces(level) + "Array");

        ParseViewState((IEnumerable) vs, level);

    }

    else if (vs.GetType() == typeof(System.String)) {

        Trace.Warn(level.ToString(), Spaces(level) + "'" + vs.ToString() + "'");

    }

    else if (vs.GetType().IsPrimitive) {

        Trace.Warn(level.ToString(), Spaces(level) + vs.ToString());

    }

    else {

        Trace.Warn(level.ToString(), Spaces(level) + vs.GetType().ToString());

    }

}

 

private void ParseViewState(Triplet vs, int level) {

    ParseViewState(vs.First, level + 1);

    ParseViewState(vs.Second, level + 1);

    ParseViewState(vs.Third, level + 1);

}

 

private void ParseViewState(Pair vs, int level) {

    ParseViewState(vs.First, level + 1);

    ParseViewState(vs.Second, level + 1);

}

 

private void ParseViewState(IEnumerable vs, int level) {

    foreach (object item in vs) {

        ParseViewState(item, level + 1);

    }

}

 

private string Spaces(int count) {

    string spaces = "";

    for (int index = 0; index < count; index++) {

        spaces += "   ";

    }

    return spaces;

}

 

    对这些代码,还没有亲自验证过,资料来的已经比较早,好像Microsoft已经打过补丁了。不过网上还是能找到Decode ViewState的工具来解析ViewState。试了一下内部的站点,大多是解析失败的,但还是找到了一个现成的案例BS2Portal

 

      

 

     BS2Portal,找到_ViewState

 

 

 

    在工具中输入BS2Portal地址,Extract 一下,如果成功,ViewState String 中显示ISVIEW,然后再ViewState 输入_ViewState值,Decode一下就能看到ViewState 中的值了。

 

   如何让ViewState更安全

 

   可以向应用程序中添加两种 ViewState 安全级别:

 

防篡改

 

加密

 

防篡改

 

 尽管散列代码不能确保 ViewState 字段中实际数据的安全,但它能够显著降低有人通过 ViewState 骗过应用程序的可能性,即防止回传应用程序通常禁止用户输入的值。

 

 

 

可以通过设置 EnableViewStateMAC 属性来指示 ASP.NET 向 ViewState 字段中追加一个散列代码:

 

%@Page EnableViewStateMAC=true %

   可以在页面级别上设置 EnableViewStateMAC,也可以在应用程序级别上设置。在回传时,ASP.NET 将为 ViewState 数据生成一个散列代码,并将其与存储在回传值中的散列代码进行比较。如果两处的散列代码不匹配,该 ViewState 数据将被丢弃,同时控件将恢复为原来的设置。

 

 

    默认情况下,ASP.NET 使用 SHA1 算法来生成 ViewState 散列代码。此外,也可以通过在 machine.config 文件中设置 <machineKey> 来选择 MD5 算法,如下所示:

 

<machineKey validation="MD5" />  

加密

 

可以使用加密来保护 ViewState 字段中的实际数据值。首先,必须如上所述设置 EnableViewStatMAC="true"。然后,将 machineKey validation 类型设置为 3DES。这将指示 ASP.NET 使用 Triple DES 对称加密算法来加密 ViewState 值。

 

<machineKey validation="3DES" />

 

 Web 领域中的 ViewState 安全性

 

 默认情况下,ASP.NET 将创建一个随机的验证密钥,并存储在每个服务器的本地安全授权 (LSA) 中。要验证在另一台服务器上创建的 ViewState 字段,两台服务器的 validationKey 必须设置为相同的值。如果要通过上述方式之一,对运行于 Web 领域配置中的应用程序进行 ViewState 安全设置,则需要为所有服务器提供一个唯一的、共享的验证密钥。

 

 

 

验证密钥是一个包含 20 到 64 位密码增强字节的随机字符串,用 40 到 128 个十六进制字符表示。密钥越长越安全,因此建议使用 128 个字符的密钥(如果计算机支持)。例如:

 

 

<machineKey validation="SHA1" validationKey="

F3690E7A3143C185AB1089616A8B4D81FD55DD7A69EEAA3B32A6AE813ECEECD28DEA66A

23BEE42193729BD48595EBAFE2C2E765BE77E006330BC3B1392D7C73F" />

 

 

 

 

 

System.Security.Cryptography 名称空间包括 RNGCryptoServiceProvider 类,使用该类可以生成此字符串,如以下 GenerateCryptoKey.aspx 示例所示: 

 

 

<%@ Page Language="c#" %>

<%@ Import Namespace="System.Security.Cryptography" %>

<HTML>

    <body>

        <form runat="server">

        <H3>生成随机加密密钥</H3>

        <P>

            <asp:RadioButtonList id="RadioButtonList1"

            runat="server" RepeatDirection="Horizontal">

                <asp:ListItem Value="40">40-byte</asp:ListItem>

                <asp:ListItem Value="128" Selected="True">128-byte</asp:ListItem>

            </asp:RadioButtonList>&nbsp;

            <asp:Button id="Button1" runat="server" onclick="GenerateKey"

            Text="生成密钥">

            </asp:Button></P>

        <P>

            <asp:TextBox id="TextBox1" runat="server" TextMode="MultiLine"

            Rows="10" Columns="70" BackColor="#EEEEEE" EnableViewState="False">

            复制并粘贴生成的结果</asp:TextBox></P>

        </form>

    </body>

</HTML>

 

 

<script runat=server>

 

   void GenerateKey(object sender, System.EventArgs e)

   {

       int keylength = Int32.Parse(RadioButtonList1.SelectedItem.Value);

      

      // 在此处放入用于初始化页面的用户代码

        byte[] buff = new Byte[keylength/2];

 

        RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();

 

        // 该数组已使用密码增强的随机字节进行填充

        rng.GetBytes(buff);

 

        StringBuilder sb = new StringBuilder(keylength);

        int i;

        for (i = 0; i < buff.Length; i++) {

            sb.Append(String.Format("{0:X2}",buff[i]));

        }

       

        // 粘贴到文本框,用户可从中复制

        TextBox1.Text = sb.ToString();

    }

 

</script>

 

   

 

   欢迎讨论

 

 

参考文献 :

 

   1   Susan Warren   ASP.NET ViewState 初探 China MSDN 11/2001

 

   2   Kaneboy's Blog View State              15/16/2004

 

   3   Paul Wilson      'ViewState: All You Wanted to Know'    11/2/2006