最新技术: ASP.NET 页的客户端

来源:百度文库 编辑:神马文学网 时间:2024/04/19 08:34:09
目录
分析 ASPX 代码
分析 HTML 客户端代码
视图状态字段
回发机制
分析类代码
软件行业的一个趋势是将许多编写代码的工作量转移到基本平台的基础结构。众多开发平台只是要求开发人员使用相对宽松的语法,在较高级别上对所需的信息进行描述,而不是按照一组严格的语法规则进行逐字节的硬编码。现在,开发人员经常使用 XML 语言来描述所需的结果,通过编译器或运行时引擎对内容进行分析,并将其处理成传统的可执行代码。
例如,Windows® Presentation Foundation(.NET Framework 3.0 的支柱之一)使用 XAML 作为基于 XML 的呈现语言,以描述表单用户界面。Microsoft AJAX 库(以前代码名为 ASP.NET“Atlas”的系统的一部分)使用其 XML-Script 元语言将相同原则应用于富文本网页(尽管从技术上看,XML-Script 不属于其核心发布内容,而是作为非官方示例技术进行共享)。XML-Script 是声明性布局语言,它将 HTML 元素和脚本组合在一起,形成虚拟的客户端控件。最终,XML-Script 为客户端页面引入了逻辑处理和功能。
使用声明性语言创作网页和表单有几个优点。通过采用此方式,服务器端组件可以更方便地生成页面和表单,而不必生成实际的 Visual Basic®、C# 或 JavaScript 代码。此外,对于诸如 Visual Studio® 这样的创作工具,声明性标记就其本质而言更容易进行设计。从体系结构角度来看,采用声明标记的方式,所指定的是页面元素的行为,而不是这些元素如何实现这类行为。这样,就可以创建更多的抽象层。
第一个利用这种模型的具体编程环境是 ASP.NET(从版本 1.0 开始)。正如大多数 Web 开发人员现在所知的,ASP.NET 页通常是在一、两个文件中进行编写的:一个 .aspx 标记文件和一个可选的代码隐藏文件。代码隐藏文件中包含了以任何受支持的编程语言(通常是 Visual Basic 或 C#)所编写的类文件。.aspx 标记文件包含形成页面结构的 HTML 标记、ASP.NET 控制标记和文字(它还可以包含代码)。此文本将在运行时进行分析,并转换成页类。这样的页类,在与代码隐藏类和一些系统生成的代码组合之后,共同形成可执行代码,以处理任何提交的数据,并生成响应,然后将其发送回客户端。
虽然这个总体模型为绝大多数 ASP.NET 开发人员所知,但还是存在很多只有少部分开发人员有深入了解的“黑洞”。MSDN®、相关书籍和在线文章对页面机制的各个方面进行了解释,但仍然缺少对页面内部机制进行的全面而统一的介绍。如果看一看 ASP.NET 页的 HTML 源代码,就会发现很多您可能几乎不了解的隐藏字段和自动插入的 JavaScript 代码块。但是,正是在这些字段和代码块的支持下,网页才能正常工作。在本专栏中,我将分析 ASP.NET 页所生成的客户端源代码。我不单要讨论如视图状态这类大家熟悉的隐藏字段,而且还会涉及到一些少有人知的隐藏字段,例如,控件状态、事件验证、事件目标和参数,以及系统提供的脚本代码。
我在此处讨论的很多实现细节均是针对当前的 ASP.NET 版本而言的。这些细节在将来的版本中会有所更改(相对于过去的版本已有了更改),因此您不应当构建任何依赖于不成文细节的运行代码。
分析 ASPX 代码
图 1 显示了一个虽然很小但可以运行的 ASP.NET 页。尽管它非常简单,但这是一个很好示例,因为它包括真实环境中的 ASP.NET 页面的典型元素:输入域、可点击的回发元素以及只读元素。
.aspx 页包含三个服务器控件:用于捕获数据的文本框、用于启动回发操作的提交按钮、用于显示只读数据的标签。在 .aspx 文件顶部,Page 指令定义了单个页面的一些全局属性。让我们看一看 Page 指令的最常用属性,比如在图 1 中显示的那些属性。<%@ Page Language="C#"AutoEventWireup="true"CodeFile="Test.aspx.cs"Inherits="Test"%>
 
大多数 Page 指令属性对页标记(即,浏览器通过 HTTP 响应接收的 HTML 代码)的影响都有限。但是,大部分 Page 属性都会影响由系统在 .aspx 标记和代码隐藏类的顶部构建的动态生成页的代码。Language 属性指定在 Visual Studio 中创作代码隐藏类所使用的语言。系统将使用相同语言生成动态页类,以处理浏览器对 .aspx 资源的请求。CodeFile 属性指示存储代码隐藏类的源文件。Inherits 属性指示在代码文件中应当作为动态生成的页类的父类的代码隐藏类的名称。最后,AutoEventWireup 属性指示是否应当使用默认命名约定将处理代码映射到 Page 事件。如果将 AutoEventWireup 设置为 True,则可以在代码文件中添加 Page_Load 方法,以处理页面的 Load 事件,并且它将自动注册到 Page 的 Load 事件。隐式命名约定指示事件处理程序将采用 Page_XXX 格式,其中,XXX 可以是在 Page 类中定义的任何公共事件的名称。如果将 AutoEventWireup 设置为 false,则必须将 Page 类事件与它的处理程序进行显式绑定。您可以在专门设计的类构造函数中执行此操作:public partial class Test : System.Web.UI.Page{public Test(){this.Load += new EventHandler(Page_Load);}...}
 
Web 服务器收到对给定 .aspx 资源的 HTTP 请求时,它会将请求转发给 ASP.NET 工作进程。该进程中驻留有 CLR,在其内部创建了一个运行时环境来处理 ASP.NET 请求。ASP.NET HTTP 运行时环境的最终目标是处理请求,即获得将嵌入 HTTP 响应中的标记(HTML、WML、XHTML 以及应用程序应当返回的任何其他标记)。负责返回请求标记的是称为 HTTP 处理程序的特殊系统组件。
HTTP 处理程序是实现了 IHttpHandler 接口的类的实例。ASP.NET Framework 提供了少量预定义的 HTTP 处理程序,以处理特定情况,或者用作处理其他或更多特定请求的基类。System.Web.UI.Page 类是 ASP.NET 中的一个最复杂的内置 HTTP 处理程序。
每个 ASP.NET 请求都会映射到一个 HTTP 处理程序。假设客户端浏览器对一个名为 test.aspx 的页面发出请求。请求将传递给 ASP.NET,并由 HTTP 运行时进行处理。运行时通过页处理程序工厂确定由 HTTP 处理程序类来处理该请求。如果在 AppDomain 中尚未提供正确的处理程序,则会动态地创建该处理程序,并将其存储在 Web 服务器计算机的 ASP.NET 临时文件夹中。对于名为 test.aspx 的页,将以类的形式创建一个名为 ASP.text_aspx 的 HTTP 处理程序。
针对给定请求的 HTTP 处理程序类的动态创建过程对于每个页面只发生一次,即在应用程序运行期间内该页面第一次被请求时进行创建(尽管来说,使用批编译时,只要应用程序内有一个页面收到了第一次请求即可生成处理程序)。如果应用程序重新启动或 Web 服务器上的页面源文件发生了修改,则动态创建的程序集将无效并被替换。图 2 显示了从基础 Page 类直到处理用户请求的动态生成类等页类的层次结构。

图 2 Page 类的层次结构 (单击该图像获得较小视图)

图 2 Page 类的层次结构 (单击该图像获得较大视图)
ASP.NET 运行时通过分析相应 .aspx 文件的源代码来创建动态页类的 Visual Basic 或 C# 源代码。每个包含 runat="server" 的标记都将映射到一个服务器控件实例。任何其他文本则映射到文字控件,并按原样一字不差地发出。Register 指令(如果有)帮助解析指向非标准控件的标记。返回到客户端浏览器的标记是通过将页面中每个服务器控件所发出的标记组合到一起而形成的。请注意,每个页通常都会发出标记,而且通常是 HTML 标记。但是,这不是必需的,并且 ASP.NET 页可以输出它需要的任何数据。

分析 HTML 客户端代码
图 3 显示了图 1 中的示例页的 HTML 输出。在该 HTML 中,服务器端 .aspx 页中看不到任何有 Page 指令的迹象。而是逐字复制 !DOCTYPE 指令。图 1 中的第一个 runat="server" 块是
标记。这意味着 Page 和 之间的任何文本都将按原样发出。在服务器上动态创建的页类的源代码中,此文本将转换成 LiteralControl 类的一个实例。 标记类似以下方式发出:
 
标记将转换为 HtmlForm 类的实例。该控件类没有相应的属性可用于设置输出标记中的 action 属性。action 属性被硬编码到当前页的 URL 中。此行为是基于 ASP.NET 平台基础的。请注意,ID 属性同一个与 name 属性值相同的值形成一对。
标记转换为 HTML 中的 元素。在这里,将添加 name 属性,以便与原来的 ID 属性匹配。请注意,如果省略 ID 属性,则可能会收到 Visual Studio 2005 发出的警告,但 ASP.NET 仍将成功编译该页。如果缺少 ID 属性,则会生成随机字符串,并将其绑定到 name 属性。 标记转换为 按钮。 标记将在客户端浏览器上转换为 HTML 的 标记。
在大多数情况下(虽然不是全部),带有 runat="server" 属性的每个标记都将生成一个对应的 HTML 标记块。ID 字符串将保证两个块之间稳定的匹配关系:一个在客户端,另一个在服务器端。在图 3 中可以看到,两个隐藏字段用于填充了 HTML 标记:__VIEWSTATE 和 __EVENTVALIDATION。

视图状态字段
__VIEWSTATE 字段的内容代表了页面最后在服务器上处理时的状态。尽管被发送到了客户端,但视图状态并不包含客户端应当使用的任何信息。存储在视图状态中的信息只涉及服务器页和它的一些子控件,并且由服务器独占读取、使用和修改。
通过采用此实现方式,视图状态可以不使用任何关键服务器资源,因此可以快速检索和使用。另一方面,正是因为视图状态与页面组合在一起,因此必然会使 HTTP 请求和响应的大小增加几千字节。注意,包含若干数据的实际页面的视图状态大小很容易达到 20KB。而每次进行上传和下载时都要包括这个额外的负载量。视图状态是 ASP.NET 的最重要功能之一,因为它可以基于诸如 HTTP 这样的无状态协议实现状态编程。虽然使用时不需要严格的条件,但视图状态很容易成为页面的负担。
通过重写代码文件类的两个方法,可以将视图状态字段的内容留在服务器上、存储在数据库、缓存或会话对象中。但请注意,将视图状态信息留在服务器上并非像一开始感觉的那样是一个顺理成章的解决办法。实际上,ASP.NET 团队选择基于页的视图状态并不是偶然的。只要用户沿着应用程序中的链接从一页导航到下一页,基于服务器的视图状态确实是个好的选择。请记住,ASP.NET 应用程序的工作方式是在同一页上进行重复发布。但是,如果用户单击“后退”按钮,情况会如何呢?为了安全起见,应当基于每个请求而不是基于每个页来维护视图状态。而且,被跟踪的请求链应当与用户通过“后退”和“前进”按钮可以到达的请求相匹配。将视图状态存储在客户端可能不是一个完美的方案,但存储在服务器上同样也有其不足。对于您的应用程序来说,更为可取的选择取决于您对应用程序的要求。
在 ASP.NET 2.0 中,__VIEWSTATE 隐藏字段包含两种类型的信息:视图状态和控件状态。开发人员可以完全禁用视图状态,并以纯粹的无状态方式运行其应用程序。只要您使用内置的控件和您自己编写的控件,或者至少是您可以访问其源代码的控件,这就不是问题。如果使用了已启用视图状态的自定义控件,情况会怎么样呢?某些控件(通常是大量第三方和自定义的控件)需要跨回发持久保存私有信息。此信息不是公共的,并且不准备对应用程序级别公开,例如,下拉面板的折叠/展开状态。此信息只能保存在视图状态。如果禁用视图状态,则控件可能会意外地失去作用。
为了缓解这一问题,ASP.NET 2.0 引入了控件状态的概念。每个服务器控件都可以将任何关键属性打包到集合,并将它存储到页面的控件状态中。控件状态保存到 __VIEWSTATE 字段,但与传统的视图状态不同,它不能被禁用,并且始终可用。开发人员通过 Page 类的一对新的可重写方法 LoadControlState 和 SaveControlState 来管理控件状态。但是,谈到 ASP.NET 2.0 中的视图状态,还应当注意到该版本采用了更为有效的新序列化算法,来使各个控件的状态有效地存储在隐藏字段中。因此,在大多数情况下,__VIEWSTATE 隐藏字段的总体大小是 ASP.NET 1.x 中的相应字段大小的一半。
前面提到过,视图状态存储在隐藏字段中,以便使它与特定的页请求明确关联。当给定页实例中的任何 HTML 元素回发时,动态生成的页类开始在服务器上运行,并使用存储在视图状态中的数据,来为页面中的控件重新创建最后所能够知道的正常状态。如果视图状态在客户端被篡改了,情况会怎么样呢?这种情况可能发生吗?默认情况下,会使用 Base64 公式对视图状态编码并进行散列处理,所得到的散列值也与视图状态一起存储。散列值是通过计算视图状态的内容外加服务器密钥得到的。一旦回发页面,页类中的代码会将视图状态的内容和散列值分离。下一步,它将基于检索到的视图状态内容和服务器密钥重新计算散列值。如果两个散列值不匹配,则引发安全异常(请参见图 4)。

图 4 不能在客户端上更改页面视图 (单击该图像获得较小视图)

图 4 不能在客户端上更改页面视图 (单击该图像获得较大视图)
如果恶意用户试图发布已修改了视图状态的假请求,情况会怎么样呢?恶意用户需要知道服务器密钥,才能为经过修改的视图状态内容生成可以在服务器上匹配的散列值。但是,服务器密钥是仅由服务器信息组成的,并且不出现在视图状态字段中。附带代码中的 tweakviewstate.aspx 页包含的脚本代码可以修改视图状态,并演示所发生的异常情况,如图 4 所示。
尽管视图状态几乎不能用于发动攻击,但它无法保证数据的机密性,除非使用加密。实际上,可以在客户端对视图状态的内容进行解码和检查,但不可能成功修改该内容以使经过更改的页状态用于服务器环境。
__EVENTVALIDATION 隐藏字段是 ASP.NET 2.0 的新增安全措施。该功能可以阻止由潜在的恶意用户从客户端发送的未经授权的请求。为了确保每个回发和回调事件来自于所期望的用户界面元素,页将在事件中添加额外的验证层。页通常通过将请求的内容与 __EVENTVALIDATION 字段中的信息进行匹配,来验证未在客户端添加额外的输入域,并且该值是在服务器已知的列表中选择的。页将在生成期间创建事件验证字段,而这是最不可能获取该信息的时刻。 像视图状态一样,事件验证字段包含散列值以防止发生客户端篡改。
控件使用 ClientScriptManager 对象的 RegisterEventForValidation 方法存储自己的安全回发相关信息。每个控件还可能会注册它自己的唯一 ID,但这种情况十分少见。列表控件还会存储列表中的所有值。支持事件验证的服务器控件通常在其 IPostBackDataHandler 接口的实现中调用 ValidateEvent 方法。如果验证失败,将引发安全异常。
可以基于每个页启用和禁用事件验证;每个控件类都通过 SupportsEventValidation 属性来启用事件验证。目前,还不能在特定控件实例上启用或禁用事件验证。
事件验证是为了仅限输入一组已知值而设置的防卫屏障。它只是将安全防护提升到更高水平,但本身不会阻止脚本注入式的攻击。
如果在启用 AJAX 的应用程序的环境中使用事件验证,则可能造成问题。在这类应用程序中,某些客户端工作可以临时创建新的输入元素,因而可能会由于出现未知元素而导致下一个回发失败。最好的应对方法是,一旦可能就在服务器上生成所有用户界面,并使用级联样式表显示属性在客户端上隐藏它。这样,您要使用的任何用户界面都将注册到事件验证字段。如果编写自定义控件,则应当用 SupportsEventValidation 属性设置该控件,以启用此功能。

回发机制
图 1 中的 ASP.NET 页会在用户单击按钮时执行回发操作。这是因为 标记将转换为 HTML 的提交 元素。单击提交输入字段时,浏览器将触发 HTML 客户端事件 onsubmit,然后根据所提交表单的内容来准备提交到服务器的新请求。所发送的 HTTP 请求包括一部分其他信息,用于计算按钮 ID。
页类将扫描 HTTP 请求的正文,以确定所发布的字段中是否有任何字段与 ASP.NET 页中按钮控件的 ID 匹配。如果找到匹配项,将调用该按钮控件以运行与它的 Click 事件关联的任何代码。更准确地说,页类将检查并确定所匹配的按钮控件是否实现了 IPostBackEventHandler 接口。如果是,它将调用该接口的 RaisePostbackEvent 方法。对于按钮控件,该方法将引发服务器端的 Click 事件。
到此为止,一切顺利。但如果页面中包含 LinkButton 控件,情况会怎么样?图 5 显示的 ASP.NET 页的标记与图 1 中的页相同,只不过使用的是 LinkButton 而不是 Submit 按钮。可以看到,标记包括另外两个隐藏字段(__EVENTTARGET 和 __EVENTARGUMENT)和一些 JavaScript 代码。链接按钮的 href 目标绑定到 __doPostback 脚本函数,这意味着一旦检测到客户端上有点击该元素的操作,就将调用这个函数。通过生成 LinkButton 控件的代码,__doPostback 函数在页中发出。它用合适的信息填充 __EVENTTARGET 和 __EVENTARGUMENT 字段,然后通过脚本触发回发。这种情况下,HTTP 回发请求的正文只包含页中的输入域,并且所回发的数据不引用 Submit 按钮。
ASP.NET 如何识别负责处理回发的控件?如果在请求正文中引用的所有控件都未实现 IPostBackEventHandler 接口,则页类将查找 __EVENTTARGET 隐藏字段(如果有)。字段的内容假定为导致回发的控件的 ID。如果此控件实现了 IPostBackEventHandler 接口,则调用 RaisePostbackEvent 方法。对于 LinkButton 控件,这将导致调用 Click 服务器事件。

分析类代码
.aspx 标记定义 ASP.NET 页的布局,并确定成员控件的大小、样式和位置。但除了可能包含某些客户端脚本代码和任何 Visual Basic 或 C# 内嵌代码以外,它不包含逻辑。初始化代码、事件处理程序和任何帮助器例程通常在单独的附带文件中提供,这些文件叫做代码隐藏文件:public partial class Test : System.Web.UI.Page{protected void Page_Load(object sender, EventArgs e){...}protected void Button1_Click(object sender, EventArgs e){...}}
 
代码文件中的类直接或间接从 System.Web.UI.Page 继承。代码文件和标记都表示必要的信息,但二者又截然不同。若要完全表示 ASP.NET 页,它们必须组合在一起以形成页类,在其中将代码文件的逻辑和标记文件的布局数据合并在一起。代码文件类已经是页类,但它缺少两部分关键的信息:用于填充用户界面的子服务器控件的列表,以及用于标识各种服务器控件的类成员的声明。
在 ASP.NET 1.x 中,页面创建人员每次将控件拖到 Web 表单上时,Visual Studio .NET 2003 都会在代码文件中自动添加新的行,以创建用于处理刚才拖动的服务器控件的类成员。这是使所有内容保持同步的非常好的步骤,但开发人员经常会碰到由于缺少类成员或存在无效的类成员而导致的编译错误。
在 ASP.NET 2.0 中,这个问题已经得到了妥当的解决。可以输入部分类,即通过源代码级、程序集受限且非面向对象的方式来扩展类的行为。在 .NET Framework 2.0 中,类定义可以跨越两个或更多个文件。每个文件都包含最终的类定义的一部分内容,编译器会考虑到合并各个部分定义,以形成单个统一的类。所有定义部分都必须有相同的签名,并且最终的类定义必须保证语法正确。
下一步,将动态生成第二个部分类,以列出所有控件成员。两个部分类将在编译时合并。系统将分析 .aspx 标记文件,以创建临时 ASP.test_aspx 类,此类继承自其最终版本的组合代码文件。 如果 ASP.NET 页未绑定到代码文件,但包含它的内嵌代码,则动态页类将继承 System.Web.UI.Page,并在它的正文中包括所有内嵌代码。
有关动态页的编译机制还有很多内容有待了解,这些内容会在以后的专栏中介绍,本文权作抛砖引玉。