表单身份验证配置和高级主题

本文档是Visual C# 教程 (转至 Visual Basic 教程)。

本教程将介绍各种不同的表单身份验证设置,以及如何通过表单元素对它们进行修改。我们将深入地探讨如何自定义表单身份验证票证的timeout 值、如何使用带有自定义URL 的登录页面(例如使用SignIn.aspx,而不是Login.aspx),以及无cookie 的表单身份验证票证。

有关该主题的更多详情,请参考以下视频:如何更改表单身份验证属性如何在 ASP.NET 应用程序中设置和使用无 Cookie 的身份验证ASP 表单登录重定位表单登录自定义键配置向身份验证方法添加自定义数据 以及使用自定义的主体对象

<<前一篇教程下一篇教程>>

简介

前一篇教程中,我们探讨了在ASP.NET 应用程序中实现表单身份验证所必需的步骤,包括在Web.config 文件中指定配置设置来创建一个登录页面,以及对已验证用户和匿名用户分别显示不同的内容。我们知道,用户可以通过将<authentication> 元素的mode 属性设置为Forms 来配置网站使用表单身份验证。另外,<authentication> 元素还可以包含一个<forms> 子元素,通过该子元素,我们可以指定各种表单身份验证设置。

本教程将介绍各种不同的表单身份验证设置,以及如何通过<forms> 元素对它们进行修改。我们将深入地探讨如何自定义表单身份验证票证的timeout 值、如何使用带有自定义URL 的登录页面(例如使用SignIn.aspx,而不是Login.aspx),以及无cookie 的表单身份验证票证。我们也将更仔细地研究表单身份验证票证的结构,以及ASP.NET 为确保票证数据不被检测和截获而采取的措施。最后,我们将讨论如何在表单身份验证票证中存储额外的用户数据,以及如何通过一个自定义主体对象来为这些数据建模。

步骤1:了解<forms> 配置设置

ASP.NET 中的表单身份验证系统提供一系列的配置设置,可根据应用进行自定义。这些设置包括:表单身份验证票证的生命周期;对票证实施何种保护;在哪种条件下使用无cookie 的身份验证票证;登录页面的路径等等。要修改这些默认值,我们应在<authentication> 元素 中添加一个 <forms> 子元素 ,像对 XML属性那样来指定希望自定义的属性值:

 

<authentication mode="Forms">
   <forms
          propertyName1="value1"
          propertyName2="value2"
        ...
          propertyNameN="valueN"
    />
</authentication>

表1 对可以通过<forms> 元素进行自定义的属性进行了汇总。由于Web.config 是一个XML 文件,在下表左侧列中的属性名称都是区分大小写的。

属性

 

说明

cookieless

该属性指定在何种条件下身份验证票证存储在cookie 中,而不是嵌入到URL 中。允许值包括:UseCookies、UseUri、AutoDetect 以及UseDeviceProfile(默认值)。步骤2 将更详细地介绍此设置。

defaultUrl

如果在查询字符串中没有指定RedirectUrl 值,则该属性指示用户从登录页面登录后将重定向到的URL。默认值为default.aspx。

domain

使用基于cookie 的身份验证票证时,该设置指定cookie 的域值。默认值为空字符串,这会导致浏览器使用发布它的域(如www.yourdomain.com )。此时,如果对子域(如 admin.yourdomain.com)进行请求,cookie 不会 发送。如果希望将 cookie传给所有的子域,我们需要自定义domain属性,将它设置为“yourdomain.com”。

enableCrossAppRedirects

一个布尔值,指示当重定向至同一服务器上其它web 应用程序的URL 时,是否记住已通过身份验证的用户。 默认值为 false。

loginUrl

登录页面的URL。默认值为login.aspx。

name

使用基于cookie 的身份验证票证时,cookie 的名称。默认值为“.ASPXAUTH”。

path

当使用基于cookie 的身份验证票证时,该设置指定cookie 的 path 属性。path 属性允许开发人员将cookie 的范围限制为特定目录结构。默认值为“/”,这会通知浏览器将身份验证票证cookie 发送给对该域的所有请求。

protection

指示使用何种技术来保护表单身份验证票证。允许值为:All (默认值)、Encryption、None 以及Validation。我们将在步骤3 中详细讨论这些设置。

requireSSL

一个布尔值,指示传送身份验证cookie 时是否需要SSL 连接。 默认值为 false。

slidingExpiration

一个布尔值,指示在同一个会话中用户每次访问站点时是否重设身份验证cookie 的 timeout。默认值为true。 我们将在“指定票证的 timeout值”一节中详细讨论身份验证票证的超时策略。

timeout

指定身份验证票证cookie 的到期时间(以分为单位)。默认值为30。 我们将在“指定票证的 timeout值”一节中详细讨论身份验证票证的超时策略。

表1:<forms> 元素属性汇总

在ASP.NET 2.0 及更高版本中,默认的表单身份验证值都是.NET Framework 的 FormsAuthenticationConfiguration 类中的固定值。所有修改都应在Web.config 文件中根据各个应用程序进行。这与ASP.NET 1.x 不同。在ASP.NET 1.x 中,默认的表单身份验证值存储在machine.config 文件中(因此,可以通过编辑machine.config 来进行修改)。关于ASP.NET 1.x 这一主题,值得一提的是,很多表单身份验证系统设置在ASP.NET 2.0 及更高版本和在ASP.NET 1.x中的默认值是不同的。如果打算将应用程序从ASP.NET 1.x 环境中迁移出来,了解这些差异非常重要。关于这些差异,请参考<forms> 元素技术文档

注意: 有几个表单身份验证设置,如 timeout、domain 和path,对最终生成的表单身份验证票证cookie 进行了详细的指定。关于cookie 的更多信息、它们的工作方式以及它们的各种属性等,请查阅Cookie 教程

指定票证的Timeout 值

表单身份验证票证是代表身份的标记。使用基于cookie 的身份验证票证时,该标记以cookie 的形式存储。每次请求时,都会将该标记发送到web 服务器。从本质上来说,拥有了该标记就相当于向系统宣布:“我是xxx,我已经登录”。这样,当用户访问页面时,系统会记住用户的身份。

表单身份验证票证中不仅包含用户的身份,还包含有助于确保标记完整性和安全性的信息。毕竟,我们并不希望恶意用户创建一个假标记或偷偷地对某个有效标记进行修改。

票证中这类信息的一个示例是 到期时间,它是票证到期的日期和时间。每次FormsAuthenticationModule 检查身份验证票证时,都要确保票证尚未过期。如果已经过期,则忽略票证,并将用户标识为匿名用户。这种安全措施可以抵御重播攻击。假如没有到期时间,如果黑客截获了用户的有效身份验证票证(可能是通过物理方式访问用户的计算机并找到用户的cookie),他们就可以用偷来的身份验证票证向服务器发送请求,从而登录。虽然到期时间不能阻止这种情况的发生,但它能限制攻击可以成功的期限。

注意: 步骤 3将详细介绍表单身份验证系统用于保护身份验证票证的其它技术。

创建身份验证票证后,表单身份验证系统通过查询timeout 设置来决定它的到期时间。正如表1 中提到的那样,timeout 的默认值为30分钟,即,创建表单身份验证票证时,其到期时间被设置为自创建日期和时间起的30 分钟。

到期时间定义的是表单身份验证票证到期的绝对时间。但开发人员通常希望实现可调节的到期时间,即,在用户每次访问站点时重设到期时间。我们将通过slidingExpiration 设置来实现该行为。如果slidingExpiration 设置为true(默认值),则每次FormsAuthenticationModule 验证用户身份时,都更新票证的到期时间。如果设置为false,则不在每次请求时更新到期时间,后果就是,一旦到了票证创建时设置的到期时间,票证就过期了。

注意: 身份验证票证中存储的到期时间是一个绝对的日期和时间值,如“August 2, 2008 11:34 AM”。并且,该日期和时间与web 服务器的本地时间相关。假设web 服务器所在的区域采用夏令时(DST,将时间提前一小时),该设计方案会有一些有趣的副作用。我们来看看,如果到期时间是DST 开始时(即2:00 AM)左右的30分钟,ASP.NET 站点会发生什么情况。假定某个用户在2008 年 3 月11 日凌晨1:55 登录网站,那么系统将会为他创建一个表单身份验证票证,其到期时间为2008 年 3 月11 日凌晨2:25(登录后的30 分钟)。然而,到了凌晨2:00,由于DST 时间开始,时钟自动跳转到凌晨3:00。当用户在登录6 分钟后(凌晨3:01)加载一个新页面时,FormsAuthenticationModule 发现票证已经过期,因此将用户重定向到登录页面。要详细了解此事、其它身份验证票证超时趣事,以及解决方法,请参阅Stefan Schackow 所著的专业ASP.NET 2.0安全、成员身份与角色管理(ISBN: 978-0-7645-9698-8)。

图1 阐述的是slidingExpiration 设置为false,且timeout 设置为30 时的工作流。注意,登录时生成的身份验证票证包含到期日期,且在以后的请求中不对该值进行更新。如果FormsAuthenticationModule 发现票证过期,则丢弃票证,并将请求视为匿名请求。

图01 :slidingExpiration 为 false 时,表单身份验证票证到期时间的图形表示(单击此处查看实际大小的图像

图2 显示的是slidingExpiration 设置为true 且timeout 设置为30 时的流程。收到一个身份验证请求(且票证未到期)时,票证的到期时间被更新。

图02 :slidingExpiration 为 true 时,表单身份验证票证到期时间的图形表示(单击此处查看实际大小的图像

使用基于cookie 的身份验证票证(默认值)时,问题就变得复杂了,因为cookie 也指定了自己的到期时间。cookie 的到期时间(或没有指定到期时间)指示浏览器何时销毁cookie。如果cookie 没有指定到期时间,则浏览器关闭时销毁cookie。如果指定了到期时间,则cookie 就一直存储在用户的计算机中,直到超过到期时间中指定的日期和时间时才被销毁。cookie 被浏览器销毁后,就不会再发送给web 服务器了。因此,cookie 的销毁类似于用户从站点注销。

注意: 当然,用户也可以主动销毁存储在计算机中的cookie。在 Internet Explorer 7 中,进入Tools、Options,单击“Browsing history” 部分中的Delete 按钮。然后,单击“Delete cookies” 按钮。

表单身份验证系统创建基于会话还是基于到期时间的cookie,这取决于传递给persistCookie参数的值。我们知道FormsAuthentication 类的 GetAuthCookie、SetAuthCookie 和 RedirectFromLoginPage 方法接受两个输入参数:usernamepersistCookie。我们在前面教程中创建的登录页面包含一个“Remember me” 复选框,它决定是否创建一个永久性cookie。永久性cookie 是基于到期时间的;而永久性cookie 是基于会话的。

前面讨论的timeout 和 slidingExpiration 概念应用到基于会话和基于到期时间这两种cookie 时都是一样的。唯一比较大的区别在于执行方面:使用基于到期时间的cookie 时,如果将slidingTimeout 设置为true,当指定的到期时间过了一半后,才对cookie 的到期时间进行更新。

我们来对网站的身份验证票证超时策略进行改动,使票证在一个小时(60分钟)后过期,并使用可调节的到期时间。为此,我们需要更新Web.config 文件,为<authentication> 元素添加一个<forms> 元素,标记如下:

<authentication mode="Forms">

<forms

  slidingExpiration=&quot;true&quot;

  timeout=&quot;60&quot;

/&gt;

</authentication>

使用 Login.aspx 以外的登录页面URL

由于 FormsAuthenticationModule 自动将未授权用户重定向到登录页面,因此它需要知道登录页面的URL。该 URL 通过<forms> 元素的loginUrl 属性指定,默认值为“login.aspx”。如果用户正在访问一个已有的网站,则可能有一个URL 不同的登录页面,该URL 已被搜索引擎标记和索引。不需要将已有的登录页面重命名为“login.aspx”,也不需要断开链接和用户的书签,用户可以修改loginUrl 属性,使其指向登录页面。

例如,如果用户的登录页面为SignIn.aspx,并位于Users 目录中,我们可以将loginUrl 配置设置指向“~/Users/SignIn.aspx”,如下:

<authentication mode="Forms">

<forms

  loginUrl=&quot;~/Users/SignIn.aspx&quot;

/&gt;

</authentication>

由于当前应用程序已有一个名为Login.aspx 的登录页面,因此不必在<forms> 元素中指定自定义值。

步骤2:使用无Cookie 的表单身份验证票证

默认情况下,表单身份验证系统根据访问站点的用户代理来决定是将身份验证票证存储在cookie 集中还是嵌入URL 中。所有主流的桌面浏览器,如Internet Explorer、Firefox、Opera 和Safari 都支持cookie,但不是所有的移动设备都支持Cookie。

表单身份验证系统使用的cookie 策略取决于<forms> 元素中的无cookie 设置,它可设置的四个取值分别是:

  • UseCookies – 指定总是使用基于 cookie 的身份验证票证。
  • UseUri – 指示从不使用基于 cookie 的身份验证票证。
  • AutoDetect – 如果设备配置文件不支持 cookie ,则不使用基于 cookie 的身份验证票证;如果设备配置文件支持cookie ,则使用探测机制来决定是否使用 cookie 。
  • UseDeviceProfile – 为默认值。如果设备配置文件支持 cookie ,就使用基于 cookie 的身份验证票证。不使用探测机制。

AutoDetect 和 UseDeviceProfile 设置都依靠一个设备配置文件 来决定是使用基于cookie的身份验证票证还是无cookie 的身份验证票证。ASP.NET 维护一个关于各种设备及其功能的数据库,数据库中的内容包括是否支持cookie 以及支持的JavaScript 版本等信息。每次,当一个设备向web 服务器请求某个网页时,它会一起发送一个标识设备类型的 用户代理 HTTP标题。ASP.NET 会自动将设备提供的用户代理字符串与数据库中指定的相应配置文件进行匹配。

注意: 设备功能数据库存储在多个 XML 文件中,这些文件位于浏览器定义文件架构 中。默认的设备配置文件位于%WINDIR%\Microsoft.Net\Framework\v2.0.50727\CONFIG\Browsers中。我们也可以在应用程序的App_Browsers 文件夹中添加自定义的文件。更多信息,请参见如何在 ASP.NET 网页中检测浏览器类型

由于默认设置为UseDeviceProfile,当访问站点的设备的配置不支持cookie 时,系统将使用无cookie 的表单身份验证票证。

在 URL 中对身份验证票证进行编码

每次对特定网站发出请求时,cookie 是存储浏览器信息的理想媒体,这也是访问设备支持cookie 时表单身份验证设置默认使用cookie 的原因。如果不支持cookie,我们必须使用其它方法将身份验证票证从客户端传送到服务器。在无cookie 环境中的通常做法是在URL 中对 cookie 数据编码。

查看这些信息在URL 中的嵌入方式的最好方法就是强制站点使用无cookie 的身份验证票证。为此,我们可以将无cookie 配置设置设为UseUri:

<authentication mode="Forms">

<forms

  cookieless=&quot;UseUri&quot;

  slidingExpiration=&quot;true&quot;

  timeout=&quot;60&quot;

/&gt;

</authentication>

完成上述更改后,通过浏览器访问站点。匿名访问时,URL 看起来和以前没有区别。例如,访问Default.aspx 页面时,浏览器地址栏显示如下的URL

https://localhost:2448/ASPNET_Security_Tutorial_03_CS/default.aspx

然而,一旦登录后,表单身份验证票证将嵌入到URL 中。例如,当访问登录页面并以Sam 的名义登录后,系统将返回到Default.aspx 页面,但此时的URL 为:

https://localhost:2448/ASPNET_Security_Tutorial_03_CS/(F(jaIOIDTJxIr12xYS-VVgkqKCVAuIoW30Bu0diWi6flQC-FyMaLXJfow_Vd9GZkB2Cv-rfezq0gKadKX0YPZCkA2))/default.aspx

表单身份验证票证已嵌入到URL 中。字符串(F(jaIOIDTJxIr12xYS-VVgkqKCVAuIoW30Bu0diWi6flQC-FyMaLXJfow_Vd9GZkB2Cv-rfezq0gKadKX0YPZCkA2)是十六进制编码的身份验证票证信息,这与通常情况下存储在cookie 中的数据相同。

为使无cookie 的身份验证票证正常运行,系统必须对页面上的所有URL 编码以包含身份验证票证数据。否则,用户单击某个链接时,身份验证票证会丢失。幸好,嵌入逻辑是自动执行的。为演示此功能,我们打开Default.aspx 页面,并添加一个HyperLink 控件,分别将其Text 和NavigateUrl 属性设置为"Test Link" 和 "SomePage.aspx"。当然,这并不是说我们的项目中真的有个页面叫SomePage.aspx。

保存对Default.aspx 的更改,然后从浏览器中访问该页面。登录该站点,这样表单身份验证票证就被嵌入到URL 中了。然后,在Default.aspx 中,单击“Test Link” 链接。 发生了什么?如果不存在SomePage.aspx 页面,则会发生一个404 错误,不过此错误在这里并不重要。注意观察浏览器的地址栏,我们会发现URL 中包含了表单身份验证票证!

https://localhost:2448/ASPNET_Security_Tutorial_03_CS/(F(jaIOIDTJxIr12xYS-VVgkqKCVAuIoW30Bu0diWi6flQC-FyMaLXJfow_Vd9GZkB2Cv-rfezq0gKadKX0YPZCkA2))/SomePage.aspx

链接中的URL “SomePage.aspx” 将自动转换成一个包含身份验证票证的URL – 我们不必写一行代码!表单身份验证票据将自动嵌入到任何超链接的URL 中,只是超链接不以"http://" 或 “/” 开头。至于超链接是出现在一个对Response.Redirect 的调用中,一个HyperLink 控件中,还是一个anchor HTML 元素(即,<a href="...">...</a>)中,这都没有关系。只要URL 不是“http://www.someserver.com/SomePage.aspx”或“/SomePage.aspx” 这样的形式,系统都会对表单身份验证票证进行嵌入处理。

注意: 无 cookie的表单身份验证票证与基于cookie 的身份验证票证采用相同的超时策略。但无cookie 的身份验证票证更容易受到重播攻击,原因是身份验证票证被直接嵌入到URL 中。设想一下,假如某个用户访问一个网站,登录后将URL 复制下来通过电子邮件发送给同事。如果他的同事在票证到期之前单击该链接,那么他们都将作为发送电子邮件的那个用户登录系统!

步骤3:保护身份验证票证

表单身份验证票证在cookie 中或直接嵌入到URL 中进行有线传输。除身份信息外,身份验证票证还可以包含用户数据(我们将在步骤4 中看到)。因此,对票证数据进行加密以避免窥探是很重要的。更重要的是,表单身份验证系统必须确保票证未被篡改。

为确保票证数据的私密性,表单身份验证系统可对票证数据进行加密。加密票证数据失败时会以明文的形式传递敏感信息。

为确保票证的真实性,表单身份验证系统必须 验证票证。验证就是确保特定数据未被修改的行为,这是通过消息身份验证代码 (MAC) 来实现的。简单的说,MAC 是一小段信息,用于识别需要进行验证的数据(此时为票证)。如果MAC 表示的数据被改动过,则MAC 和数据不匹配。此外,对某个黑客来说,既要改动数据,还要根据修改的数据生成一个自己的MAC,这实在很困难。

表单身份验证系统创建(或改动)票证后,会生成一个MAC 并将其附加在票证数据中。后续的请求到达时,表单身份验证系统将MAC 与票证数据进行比较,以验证票证数据的真实性。图3以图表的形式阐述了该流程。

图03:通过MAC 确保票证的真实性(单击此处查看实际大小的图像

对身份验证票证运用何种安全措施取决于<forms> 元素中的protection设置。protection 可设置的三个取值分别为:

  • All – 对票证进行加密和数字签名(默认值)。
  • Encryption – 只加密 – 不生成 MAC 。
  • None – 对票证既不进行加密也不进行数字签名。
  • Validation – 生成 MAC ,但票证数据以明文形式发送。

Microsoft 强烈推荐使用All 配置。

设置验证和加密密钥

表单身份验证系统加密和验证身份验证票证时使用的加密和哈希算法可以在Web.config 文件的<machineKey>元素 中自定义。表 2列出了<machineKey> 元素的属性以及可能的取值。

属性

说明

decryption

指示加密使用的算法。该属性的值可以是以下4 个取值之一:

  • Auto – 默认值;根据属性 decryptionKey 的长度来决定算法。
  • AES – 使用高级加密标准 (AES) 算法。
  • DES – 使用数据加密标准 (DES) 算法。该算法的计算能力弱,不建议使用。
  • 3DES – 使用三重 DES 算法。该算法应用 DES 算法三次。

decryptionKey

加密算法使用的密钥。该值必须为适当长度(取决于decryption 中的值)的十六进制字符串、含有“AutoGenerate”,或者是附加有“,IsolateApps” 的值。附加“IsolateApps” 指示 ASP.NET 为每个应用程序使用唯一的值。默认值为“AutoGenerate,IsolateApps”。

validation

指示验证使用的算法。该属性的值可以是以下4 个取值之一:

  • AES – 使用高级加密标准 (AES) 算法。
  • MD5 – 使用 消息摘要5 (MD5) 算法。
  • SHA1 – 使用 SHA1 算法(默认值)。
  • 3DES – 使用三重 DES 算法。

validationKey

验证算法使用的密钥。该值必须为适当长度(取决于validation 中的值)的十六进制字符串、含有“AutoGenerate”,或者是附加有“,IsolateApps” 的值。附加“IsolateApps” 指示 ASP.NET 为每个应用程序使用唯一的值。默认值为“AutoGenerate,IsolateApps”。

表2:<machineKey> 元素属性

对这些encryption 和 validation 选项的深入讨论,以及各种算法优缺点的探讨不在本文章的范畴之内。对这些问题的深入探讨,包括使用哪种加密和验证算法、密匙的长度以及生成这些密钥的最佳方式,请参考 专业ASP.NET 2.0安全、成员身份与角色管理

默认情况下,系统会自动为每个应用程序生成用于加密和验证的密钥,这些密钥都存储在本地安全授权(LSA) 中。简而言之,默认设置可保证对于每个web 服务器和应用程序,密钥都是唯一的。因此,这种默认行为在如下两种情况下行不通:

  • Web场 – 在 web 场场景下,一个 web 应用程序分散在多个 web 服务器上,以便获得可伸缩性和冗余。每个传入的请求被分派到场中的一个服务器上,这就意味着在用户的会话期间,每个服务器都可能要处理他的请求。因此,每个服务器都必须使用相同的加密和验证密钥,这样,在一台服务器上创建、加密以及验证的表单身份验证票证才能在场中的另一台服务器上进行解密和验证。
  • 跨应用程序票证共享 – 一个 web 服务器可能运行多个 ASP.NET 应用程序。如果用户需要让不同的应用程序共用一个表单身份验证票证,则必须使这些应用程序的加密和验证密钥匹配。

使用 web 场设置或同一服务器上的多个应用程序间共用身份验证票证时,用户必须在受影响的应用程序中配置<machineKey> 元素,保证它们的decryptionKey 和 validationKey 值互相匹配。

虽然我们的示例应用程序不属于上述两种情况,我们也可以显式地指定decryptionKey 和 validationKey 值,并定义要使用的算法。在Web.config 文件中添加一个<machineKey> 设置:

<configuration> 

<system.web>

... Some markup was removed for brevity ...

&lt;machineKey

    decryption=&quot;AES&quot;

    validation=&quot;SHA1&quot;

    decryptionKey=&quot;1513F567EE75F7FB5AC0AC4D79E1D9F25430E3E2F1BCDD3370BCFC4EFC97A541&quot;

    validationKey=&quot;32CBA563F26041EE5B5FE9581076C40618DCC1218F5F447634EDE8624508A129&quot;

  /&gt;

</system.web>

</configuration>

更多详情,请参阅如何在 ASP.NET 2.0 中配置 MachineKey

注意:decryptionKey 和 validationKey 值来自Steve GibsonPerfect Passwords网页,每次访问该页面时,它生成64 个随机的十六进制字符。为减少这些密钥进入产品应用程序的可能性,建议使用Perfect Passwords 页面随机生成的密钥替换上述密钥。

步骤4:在票证中存储其它用户数据

很多 web 应用程序显示当前登录用户的信息。例如,一个web 页面可能在页面上面的一角显示用户的名称以及她上次登录的日期。表单身份验证票证存储了当前登录用户的用户名,但如果需要其它的信息,页面必须到用户存储(一般为数据库)查找身份验证票证中没有存储的信息。

我们只需要编写少量代码就可以在表单身份验证票证中存储其它用户信息。可使用FormsAuthenticationTicket类 的 UserData属性 来存储这些数据。该属性是存储最常用的与用户有关的少量信息的理想之地。UserData 属性中指定的值是身份验证票证cookie 的一部分。与其它票证域一样,该值根据表单身份验证系统的配置来进行加密和验证。默认情况下,UserData 是一个空字符串。

为了在身份验证票证中存储用户数据,我们需要在登录页面中编写一些代码,获取与用户有关的信息并存储在票证中。由于UserData 是一个字符串类型的属性,因此存储在该属性中的数据必须要序列化为一个字符串。例如,假定我们的用户存储包括每个用户的出生日期和雇主名称,我们希望将这两个属性值存储在身份验证票证中。我们可以在用户的出生日期字符串后跟一个管道符(“|”),管道符后再跟雇主名称,从而将这些值序列化到一个字符串中。对于出生在1974 年 8 月15 日并为Northwind Traders 工作的用户,我们可用为UserData 属性分配字符串:“1974-08-15|Northwind Traders”。

当我们需要访问存储在票证中的数据时,我们可以获取当前请求的FormsAuthenticationTicket,并对UserData 属性进行反序列化。在出生日期和雇员名称示例中,我们可以根据分隔符(“|”),将 UserData 字符串拆分成两个子串。

图04 : 其它用户信息可存储在身份验证票证中(单击此处查看实际大小的图像

将信息写入UserData

不幸的是,将与用户有关的信息添加到表单身份验证票证并不像大家期望的那么简单。FormsAuthenticationTicket 类的 UserData 属性是只读属性,只能通过FormsAuthenticationTicket 类的构造函数进行指定。当我们在构造函数中指定UserData 属性时,我们还需要提供票证的其它值:username、issue date 以及 expiration 等。在前面的教程中,我们创建登录页面时,FormsAuthentication 类自动帮我们进行了处理。在FormsAuthenticationTicket 中添加UserData 时,我们需要编写代码复制大部分FormsAuthentication 类已提供的函数。

让我们更新Login.aspx 页面,在身份验证票证中记录其它的用户有关信息,从而探讨处理UserData 时的必需代码。假设我们的用户存储中包含用户公司的名称,以及用户的职称,而且我们想在身份验证票证中获取这些信息。对Login.aspx 页面的LoginButton Click 事件处理程序进行更新,代码如下所示:

protected void LoginButton_Click(object sender, EventArgs e)

{

// Three valid username/password pairs: Scott/password, Jisun/password, and Sam/password.

string[] users = { &quot;Scott&quot;, &quot;Jisun&quot;, &quot;Sam&quot; };

string[] passwords = { &quot;password&quot;, &quot;password&quot;, &quot;password&quot; };

string[] companyName = { &quot;Northwind Traders&quot;, &quot;Adventure Works&quot;, &quot;Contoso&quot; };

string[] titleAtCompany = { &quot;Janitor&quot;, &quot;Scientist&quot;, &quot;Mascot&quot; };

for (int i = 0; i &lt; users.Length; i++)

{

    bool validUsername = (string.Compare(UserName.Text, users[i], true) == 0);

    bool validPassword = (string.Compare(Password.Text, passwords[i], false) == 0);

    if (validUsername &amp;&amp; validPassword)

    {

        // Query the user store to get this user&#39;s User Data

        string userDataString = string.Concat(companyName[i], &quot;|&quot;, titleAtCompany[i]);

        // Create the cookie that contains the forms authentication ticket

        HttpCookie authCookie = FormsAuthentication.GetAuthCookie(UserName.Text, RememberMe.Checked);

        // Get the FormsAuthenticationTicket out of the encrypted cookie

        FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(authCookie.Value);

        // Create a new FormsAuthenticationTicket that includes our custom User Data

        FormsAuthenticationTicket newTicket = new FormsAuthenticationTicket(ticket.Version, 
        ticket.Name, ticket.IssueDate, ticket.Expiration, ticket.IsPersistent, userDataString);

        // Update the authCookie&#39;s Value to use the encrypted version of newTicket

        authCookie.Value = FormsAuthentication.Encrypt(newTicket);

        // Manually add the authCookie to the Cookies collection

        Response.Cookies.Add(authCookie);

        // Determine redirect URL and send user there

        string redirUrl = FormsAuthentication.GetRedirectUrl(UserName.Text, RememberMe.Checked);

        Response.Redirect(redirUrl);

    }

}

// If we reach here, the user&#39;s credentials were invalid

InvalidCredentialsMessage.Visible = true;

}

我们来逐行进行分析。该方法首先定义了4 个字符串数组:users、passwords、companyName 和 titleAtCompany。这些数组用于存储系统中用户帐户的用户名、密码、公司名称、以及职称,这里有3 个用户:Scott、Jisun 和 Sam。在实际的应用程序中,这些值都是从用户存储中查询得到的,而不是在页面源代码中的固定值。

在前面的教程中,如果用户提供的凭据有效,我们只调用FormsAuthentication.RedirectFromLoginPage(UserName.Text, RememberMe.Checked),它执行以下步骤:

  1. 创建表单身份验证票证。
  2. 将票证写入相应的存储。对基于 cookie 的身份验证票证,使用的是浏览器的 cookie 集;而对无 cookie 的身份验证票证,系统将票证数据序列化到URL 中。
  3. 将用户重定向到适当的页面。

这些步骤在上述代码中是重复的。首先,我们用管道符(“|”) 将公司名称和用户职称合并成一个字符串,作为最终存储在UserData 属性中的字符串。

string userDataString = string.Concat(companyName[i], "|", titleAtCompany[i]);

接下来,调用FormsAuthentication.GetAuthCookie 方法,该方法创建身份验证票证,根据配置进行加密和验证,并将它分配给一个HttpCookie 对象。

HttpCookie authCookie = FormsAuthentication.GetAuthCookie(UserName.Text, RememberMe.Checked);

为了处理嵌入cookie 中的 FormAuthenticationTicket,我们需要调用FormAuthentication 类的Decrypt 方法 ,传入 cookie值。

FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(authCookie.Value);

然后,根据已有的FormsAuthenticationTicket 值,我们创建一个 新的 FormsAuthenticationTicket 实例。但这个新票证包含与用户有关的信息(userDataString)。

FormsAuthenticationTicket newTicket = new FormsAuthenticationTicket(ticket.Version, ticket.Name, ticket.IssueDate, ticket.Expiration, ticket.IsPersistent, userDataString);

我们调用Encrypt方法 对新 FormsAuthenticationTicket 实例进行加密(和验证),并将加密(和验证)完的数据放回authCookie。

authCookie.Value = FormsAuthentication.Encrypt(newTicket);

最后,将 authCookie 添加到 Response.Cookies 集,并调用 GetRedirectUrl 方法来确定将用户导航到的页面。

Response.Cookies.Add(authCookie);

string redirUrl = FormsAuthentication.GetRedirectUrl(UserName.Text, RememberMe.Checked);

Response.Redirect(redirUrl);

所有这些代码都是必须的,因为UserData 属性是只读属性,且FormsAuthentication 类的 GetAuthCookie、SetAuthCookie 或 RedirectFromLoginPage 方法中都没有提供任何指定UserData 信息的途径。

注意: 我们刚刚探讨的代码在基于 cookie 的身份验证票证中存储用户有关的信息。将表单身份验证票证序列化到URL 是 .NET Framework 内部的类完成的。简而言之,我们不能将用户数据存储在无cookie 的表单身份验证票证中。

访问 UserData 信息

此时,当用户登录时,用户的公司名称和职称都存储在表单身份验证票证的UserData 属性中。在任何一个页面上,无需查询用户存储,我们就可以从身份验证票证中访问这些信息。为演示如何从UserData 属性中提取这些信息,我们对Default.aspx 页面进行更新,使欢迎信息中不但包含用户名,还包含公司名称和用户职称。

目前,Default.aspx 页面包含一个AuthenticatedMessagePanel 面板,里面有一个名为WelcomeBackMessage 的标签控件。该面板是面向已验证身份的用户的。对Default.aspx 页面的Page_Load 事件处理程序中的代码进行更新,如下所示:

protected void Page_Load(object sender, EventArgs e)

{

if (Request.IsAuthenticated)

{

    WelcomeBackMessage.Text = &quot;Welcome back, &quot; + User.Identity.Name + &quot;!&quot;;

    // Get User Data from FormsAuthenticationTicket and show it in WelcomeBackMessage

    FormsIdentity ident = User.Identity as FormsIdentity;

    if (ident != null)

    {

        FormsAuthenticationTicket ticket = ident.Ticket;

        string userDataString = ticket.UserData;

        // Split on the |

        string[] userDataPieces = userDataString.Split(&quot;|&quot;.ToCharArray());

        string companyName = userDataPieces[0];

        string titleAtCompany = userDataPieces[1];

        WelcomeBackMessage.Text += string.Format(&quot; You are the {0} of {1}.&quot;, titleAtCompany, companyName);

    }

    AuthenticatedMessagePanel.Visible = true;

    AnonymousMessagePanel.Visible = false;

}

else

{

    AuthenticatedMessagePanel.Visible = false;

    AnonymousMessagePanel.Visible = true;

}

}

如果 Request.IsAuthenticated 为 true,则先将WelcomeBackMessage 的 Text 属性设置为“Welcome back, username”。然后,将User.Identity 属性分配给一个FormsIdentity 对象,这样我们就可以访问基础FormsAuthenticationTicket。一旦获取到FormsAuthenticationTicket,我们就将UserData 属性反序列化成公司名称和职称。反序列化操作跟据管道字符来拆分字符串。然后,系统将公司名称和用户职称显示在WelcomeBackMessage 标签中。

图5 显示的是实际的屏幕截图。以Scott 身份登录,屏幕将显示包含Scott 的公司名称和职称的欢迎消息。

图05 : 显示当前登录用户的公司和用户职称(单击此处查看实际大小的图像

注意: 身份验证票证的 UserData属性是用户存储的一个缓存。与其它缓存一样,当基础数据发生改变时,需要对该缓存进行更新。例如,如果有一个网页,用户可以通过它来更新其配置文件,则缓存在UserData 属性中的域必须刷新以反映用户所作的改动。

步骤5:使用自定义的主体对象

每次接收到请求时,FormsAuthenticationModule 都会验证用户的身份。如果身份验证票证未到期,FormsAuthenticationModule 就将 HttpContext.User 属性分配给一个新的GenericPrincipal 对象。该GenericPrincipal 对象有一个FormsIdentity 类型的Identity,它包含对表单身份验证票证的引用。GenericPrincipal 类仅包含实现IPrincipal 的类所需要的最少功能– 它只有一个Identity 属性和一个IsInRole 方法。

主体对象有两个职责:指出用户属于什么角色以及提供身份信息。这两个职责分别通过IPrincipal 接口的IsInRole(roleName) 方法和Identity 属性来实现。GenericPrincipal 类允许通过它的构造函数指定角色名称字符串数组,而其IsInRole(roleName) 方法仅检查传入的roleName 是否在该字符串数组中。FormsAuthenticationModule 创建 GenericPrincipal 时,向GenericPrincipal 的构造函数传入一个空的字符串数组。因此,调用IsInRole 时,总是返回false。

对大多数没有用到角色的、基于表单的身份验证情景而言,GenericPrincipal 类是满足其需要的。对那些使用默认角色处理无法满足需求的情况,或者需要将用户与一个自定义的IIdentity 对象关联起来的情况,我们可以在身份验证流程中创建一个自定义的IPrincipal 对象,并将其分配给HttpContext.User 属性。

注意: 正如我们将在以后的教程中看到的那样,启用ASP.NET 角色框架时,它创建一个类型为RolePrincipal的自定义主体对象,并覆盖表单身份验证创建的GenericPrincipal 对象。这是为了对主体对象的IsInRole 方法进行自定义,从而与角色框架的API 相接。

由于我们现在还没有涉及到角色,我们此时创建自定义主体对象的唯一理由就是将自定义的IIdentity 对象与主体对象关联起来。在步骤4 中,我们探讨了如何在身份验证票证的UserData 属性中存储其它用户信息(在本示例中是用户的公司名称和职称)。然而,UserData 信息只能通过身份验证票证进行访问,而且是一个序列化的字符串。这就意味着,我们想查看存储在票证中的用户信息时,必须对UserData 属性进行解析。

通过创建一个实现IIdentity 并包含CompanyName 和 Title 属性的类,可以提升开发人员的体验。那样的话,开发人员就可以通过CompanyName 和 Title 属性直接访问当前登录用户的公司名称和职称,而无需了解如何对UserData 属性进行解析。

创建自定义的Identity 和 Principal 类

在本教程中,我们在App_Code 文件夹中创建自定义的主体对象和标识对象。首先,在项目中添加一个App_Code 文件夹 – 在解决方案资源管理器中右键单击项目名称,选择Add ASP.NET Folder 选项,然后选择App_Code。App_Code 文件夹是一个特殊的ASP.NET 文件夹,用于存放与网站有关的类文件。

注意: 只有通过网站项目模型对项目进行管理时才使用App_Code 文件夹。如果用户使用的是Web 应用程序项目模型 ,则创建一个标准的文件夹,并在其中添加类。例如,创建一个名为Classes 的新文件夹,并在其中存放代码。

然后,在App_Code 文件夹中添加两个新的类文件,它们的名称分别是CustomIdentity.cs 和 CustomPrincipal.cs。

图06 : 在项目中添加 CustomIdentity 和 CustomPrincipal 类(单击此处查看实际大小的图像

CustomIdentity 类负责实现IIdentity 接口,而IIdentity 接口用于定义AuthenticationType、IsAuthenticated 和 Name 属性。除了这些必需的属性外,我们还希望展示基础表单身份验证票证以及用户的公司名称和职称属性。在CustomIdentity 类中输入以下代码。

using System;

using System.Data;

using System.Configuration;

using System.Web;

using System.Web.Security;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.Web.UI.WebControls.WebParts;

using System.Web.UI.HtmlControls;

public class CustomIdentity : System.Security.Principal.IIdentity

{

private FormsAuthenticationTicket _ticket;

public CustomIdentity(FormsAuthenticationTicket ticket)

  {

    _ticket = ticket;

}

public string AuthenticationType

{

    get { return &quot;Custom&quot;; }

}

public bool IsAuthenticated

{

    get { return true; }

}

public string Name

{

    get { return _ticket.Name; }

}

public FormsAuthenticationTicket Ticket

{

    get { return _ticket; }

}

public string CompanyName

{

    get 

    {

        string[] userDataPieces = _ticket.UserData.Split(&quot;|&quot;.ToCharArray());

        return userDataPieces[0];

    }

}

public string Title

{

    get 

    {

        string[] userDataPieces = _ticket.UserData.Split(&quot;|&quot;.ToCharArray());

        return userDataPieces[1];

    }

}

}

注意,该类包含一个FormsAuthenticationTicket 成员变量(_ticket),必须通过构造函数提供该票证信息。票证数据用于返回标识的Name;其 UserData 属性经过解析后返回CompanyName 和 Title 属性的值。

接下来,创建CustomPrincipal 类。因为此时我们不关心角色,因此CustomPrincipal 类的构造函数仅接受一个CustomIdentity 对象,其IsInRole 方法总是返回false。

using System;

using System.Data;

using System.Configuration;

using System.Web;

using System.Web.Security;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.Web.UI.WebControls.WebParts;

using System.Web.UI.HtmlControls;

public class CustomPrincipal : System.Security.Principal.IPrincipal

{

private CustomIdentity _identity;

  public CustomPrincipal(CustomIdentity identity)

  {

    _identity = identity;

  }

public System.Security.Principal.IIdentity Identity

{

    get { return _identity; }

}

public bool IsInRole(string role)

{

    return false;

}

}

将 CustomPrincipal 对象分配给传入请求的安全上下文

现在,我们有一个扩展默认IIdentity 规范的类,它包含CompanyName 和 Title 属性,以及一个使用自定义标识的自定义principal 类。我们现在将进入ASP.NET 管道,将自定义的主体对象赋值给传入请求的安全上下文。

ASP.NET 管道收到一个请求后,通过一系列的步骤对它进行处理。每一步都会触发一个特定的事件,这就为开发人员进入ASP.NET 管道并在其生命周期的某一点上对请求进行修改提供了可能。例如,FormsAuthenticationModule 等待 ASP.NET 触发AuthenticateRequest 事件 ,此时它针对身份验证票证检查传入请求。如果发现身份验证票证,则创建一个GenericPrincipal 对象,并分配给HttpContext.User 属性。

AuthenticateRequest 事件之后,ASP.NET 管道触发PostAuthenticateRequest事件 ,在该事件中,我们可以将 FormsAuthenticationModule 创建的GenericPrincipal 对象替换为CustomPrincipal 对象的一个实例。图7 描述了该流程。

图07 : 在 PostAuthenticationRequest 事件中将 GenericPrincipal 替换为 CustomPrincipal (单击此处查看实际大小的图像

为执行响应 ASP.NET 管道事件的代码,我们可以在 Global.asax 中创建相应的事件处理程序,或者创建自己的HTTP 模块。对于本教程,我们在 Global.asax 中创建事件处理程序。首先,将 Global.asax 添加到网站。在解决方案资源管理器中右键单击项目名称,添加一个名为Global.asax 的 Global Applicatio

图08 : 在网站中添加一个 Global.asax 文件(单击此处查看实际大小的图像

默认的Global.asax 模板包含一些ASP.NET 管道事件的事件处理程序,包括Start、End 以及Error 事件等等。我们可以随意删除这些事件处理程序,因为我们的应用程序不需要使用它们。我们关心的事件是PostAuthenticateRequest。更新Global.asax 文件,其标记应如下所示:

<%@ Application Language="C#" %>

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

<%@ Import Namespace="System.Threading" %>

<script runat="server">

void Application_OnPostAuthenticateRequest(object sender, EventArgs e)

{

    // Get a reference to the current User

    IPrincipal usr = HttpContext.Current.User;

    // If we are dealing with an authenticated forms authentication request

    if (usr.Identity.IsAuthenticated &amp;&amp; usr.Identity.AuthenticationType == &quot;Forms&quot;)

    {

        FormsIdentity fIdent = usr.Identity as FormsIdentity;

        // Create a CustomIdentity based on the FormsAuthenticationTicket           

        CustomIdentity ci = new CustomIdentity(fIdent.Ticket);

        // Create the CustomPrincipal

        CustomPrincipal p = new CustomPrincipal(ci);

        // Attach the CustomPrincipal to HttpContext.User and Thread.CurrentPrincipal

        HttpContext.Current.User = p;

        Thread.CurrentPrincipal = p;

    }

}

</script>

Application_OnPostAuthenticateRequest方法只在ASP.NET 运行时触发PostAuthenticateRequest 事件时才执行,该事件在每个传入页面请求抵达时发生一次。事件处理程序首先检查用户是否通过了身份验证,且验证方式是否为表单身份验证。如果是,则创建一个新的CustomIdentity 对象,并将当前请求的身份验证票证传给它的构造函数。接下来,创建一个CustomPrincipal 对象,并将刚才创建的CustomIdentity 对象传递给它的构造函数。最后,将当前请求的安全上下文分配给最近创建的CustomPrincipal 对象。

注意最后一步– 将 CustomPrincipal 对象与请求的安全上下文关联起来– 将主体对象赋值给两个属性:HttpContext.User 和 Thread.CurrentPrincipal。这两个赋值是必需的,这是由ASP.NET 处理安全上下文的方式决定的。.NET Framework 将一个安全上下文与每个运行线程关联起来。可通过Thread对象 的 CurrentPrincipal属性 ,以 IPrincipal 对象的形式来获得这些信息。容易让人困惑的是ASP.NET 也有自己的安全上下文信息(HttpContext.User)。

在某些情况下,确定安全上下文时检查Thread.CurrentPrincipal 属性;而在其它情况下,使用HttpContext.User 属性。例如,.NET中有一些安全特性,允许开发人员声明哪些用户或角色可以实例化某个类或调用特定的方法(参见使用 PrincipalPermissionAttribute 向业务和数据层添加授权规则 )。在内部,这些声明技术通过 Thread.CurrentPrincipal 属性来确定安全上下文。

在其它情况下,使用HttpContext.User 属性。例如,在前面的教程中,我们使用该属性来显示当前登陆用户的用户名。显然,Thread.CurrentPrincipal 属性和HttpContext.User 属性中的安全上下文必须匹配。

ASP.NET 运行时自动为我们同步这些属性。然而,该同步发生在AuthenticateRequest 事件之后,PostAuthenticateRequest 事件之前。因此,在PostAuthenticateRequest 事件中添加自定义的主体对象时,我们需要手动地为Thread.CurrentPrincipal 赋值,不然Thread.CurrentPrincipal 和 HttpContext.User 不会同步。有关该问题的更详细讨论,参见Context.User 与 Thread.CurrentPrincipal

访问 CompanyName 和 Title 属性

任何时候,当请求抵达并分派到ASP.NET 引擎时,都会触发Global.asax 文件中的Application_OnPostAuthenticateRequest事件处理程序。如果请求成功通过FormsAuthenticationModule 的身份验证,该事件处理程序将创建一个新的CustomPrincipal 对象,该对象包含一个基于表单身份验证票证的CustomIdentity 对象。完成这些逻辑处理后,我们就可以非常方便地访问有关当前登录用户的公司名称和职称的信息。

返回到Default.aspx 页面的Page_Load 事件处理程序,在步骤4 中,我们在该事件处理程序中写入了提取表单身份验证票证和解析UserData 属性的代码,以显示用户的公司名称和职称。现在,由于使用的是CustomPrincipal 和 CustomIdentity 对象,我们不需要解析票证的UserData 属性值。而仅需要获取对CustomIdentity 对象的引用,并使用其CompanyName 和 Title 属性:

CustomIdentity ident = User.Identity as CustomIdentity;

if (ident != null)

WelcomeBackMessage.Text += string.Format(&quot; You are the {0} of {1}.&quot;, ident.Title, ident.CompanyName);</code></pre>

小结

在本教程中,我们探讨了如何通过Web.config 文件来自定义表单身份验证系统的设置。我们还研究了如何处理身份验证票证的到期时间,以及加密和验证安全措施如何防止票证被窥探和修改。最后,我们讨论了使用验证票证的UserData 属性在票证中存储其它用户信息,以及如何使用自定义的主体对象和标识对象来让开发人员更方便地访问这些信息。

本教程结束了对ASP.NET 表单身份验证的讨论。在下一篇教程中,我们将开始探讨成员身份框架。

快乐编程!

 

下一篇教程