[导读]在安全已经成为首要问题的今天,ASP.NET 2.0给了开发人员极大的信心,从新引入的服务器段安全控件、角色定义、密码恢复及其基于成员和角色的编程,ASP.NET 2.0让您的系统更加安全可靠。
新的安全性功能是ASP.NET 2.0中的一项重大改进。这些功能包括管理用户帐户数据库的成员身份服务、哈希密码、管理用户角色成员身份的角色管理器,以及可以更容易实现窗体身份验证的五个新的服务器端控件。ASP.NET 2.0还提供了提供程序模型(Provider Model),使您能够完全控制Membership和Role服务以及无cookie窗体身份验证的实现。您还可以非常轻松地对用户帐户和角色进行简单的、基于Web的本地和远程管理,并且可以获得对其他非安全性相关设置的增强控制。
重推窗体身份验证
窗体身份验证是ASP.NET 1.0中使用最广泛的功能之一,因为它封装了许多特定实现所缺少的最佳实践。例如,您知道有多少窗体身份验证实现可以保护用来存放客户端凭证的cookie的完整性吗?窗体身份验证不仅将用户名写入该cookie中,而且还添加了一个消息身份验证代码(一个根据该cookie形成的哈希值和只有Web服务器知道的秘密值)。这可以防止恶意的客户端通过提高权限或通过修改其cookie中的名称来查看其他用户的数据。
如果留意.NET Web开发人员推出的各种新闻组和列表服务器,您将了解到人们正在一遍又一遍地实现相同的东西:用户数据库、cookie中缓存的角色、捕获用户名和密码的控件、管理用户和角色的工具。ASP.NET小组已经提供了针对几乎所有这些问题的内置解决方案。在研究ASP.NET 2.0的alpha版本时,我发现在以可管理的方式构建使用窗体身份验证的Web站点时,所使用的代码量急剧减少。
入门
如果您有ASP.NET 2.0(可用于MSDN),那么在演练某些可以执行的实验时,您就会发现开始使用这些新功能是多么地简单!普通订阅者可以下载。
要开始下载,您需要一个指向空目录的虚拟目录。您必须确保ASP.NET辅助进程具有读取、执行和写入该目录的权限。如果您运行的是Windows 2000或Windows XP,则需要为ASP.NET本地帐户赋予这些权限;而对于Windows Server 2003,您需要为网络服务帐户赋予这些权限。
我将使用窗体身份验证,所以我需要通过web.config文件来启用它。如果我现在向您展示如何使用ASP.NET 1.1,那么您需要打开一个文本编辑器并开始手动键入XML。但是在ASP.NET 2.0中,我最喜欢的功能之一是交互式配置文件编辑器,它直接构建于IIS管理控制台,您可以在虚拟目录的属性表的“ASP.NET”选项卡中找到它。单击“Edit configuration”按钮以打开编辑器。
图1 配置编辑器
图1显示了这个新编辑器。您会看到我选择了窗体身份验证而不是默认选项:Windows身份验证。在您自己的虚拟目录中进行同样的操作。当使用配置工具时,将Web应用程序的默认语言设置为C#,因为它将替您省去一些后面需要进行的输入。“Page Language Default”设置是Application选项卡上的第一个下拉选项。在应用这些更改之后,您将在目录中找到web.config文件并带有所有设置。
您需要向Membership服务注册一些用户以便开始,因此编写的第一页是允许添加用户的页。在该测试版中提供了一个服务器控件,通过该控件,您可以使用下面三行代码来实现该页:
<form runat='server'>
<asp:createuser runat='server'/>
</form>
<form runat='server'>
<table>
<tr><td>Name:</td><td><asp:textbox id='name' runat='server'/></td></tr>
<tr><td>Email address:</td><td><asp:textbox
id='email' runat='server'/>
</td></tr>
<tr><td>Password:</td><td><asp:textbox textmode='Password'
id='pwd1' runat='server'/>
</td></tr>
<tr><td>Retype password:</td><td><asp:textbox textmode='Password'
id='pwd2' runat='server'/>
</td></tr>
</table>
<asp:button text='Submit' οnclick='onSubmit' runat='server'/>
<p><asp:label id='msg' runat='server'/></p>
</form>
<script runat='server'>
void onSubmit(object sender, EventArgs args)
{
if (pwd1.Text.Equals(pwd2.Text))
{
MembershipCreateStatus status;
MembershipUser newUser = Membership.CreateUser(name.Text,
pwd1.Text, email.Text, out status);
msg.Text = status.ToString();
}
else
msg.Text = "Passwords don't match, try again.";
}
</script>
图2 adduser.aspx
不过,由于我使用的版本是alpha,所以我必须直接使用Membership类来手工为这个特殊的窗体编写代码。而现在,只需使用图2 中所示的ASPX页就可以了,我将在本文稍后讨论Membership类。图3显示当您将浏览器指向该页时所看到的内容。继续进行试验,现在添加一些用户和密码。成功后,您的工作应该更加轻松!
图3 成员页
完成用户添加之后,请仔细查看虚拟目录。您应该看到一个名为“DATA”新的子目录,其中包含了一个Microsoft® Access数据库。这是Membership和Role服务默认存储其数据的地方,但是稍后我会向您展示如何覆盖默认存储机制以使用SQL Server,或您自己的自定义数据储备库。现在,您可以使用ASP.NET 2.0中的安全控件了。
服务器端安全控件
图4 列出了ASP.NET 2.0中的五个新安全控件。从LoginStatus控件开始探索是个好主意。首先创建一个包含该控件的新ASPX页。为了简单起见,调用新页面default.aspx:
<form runat='server'>
<asp:loginstatus runat='server'/>
</form>
控件 | 描述 |
---|---|
LoginStatus | 提供一个登录或者注销的按钮,主要依赖于用户的当前状态 |
Login | 提供一个表单用于收集和校验依赖于用户数据库的登录凭证 |
LoginName | 显示一个已经登录的用户名 |
LoginView | 显示特定的内容,依赖于用户是否登录,而用户是否是一个角色的成员是可选的 |
PasswordRecovery | 提供一个找回忘记的密码需要的清单 |
图4 ASP.NET 2.0新的安全控件
将浏览器指向该页面,您应该看到一个Login链接。如果您在浏览器中查看结果页面的源代码,您将看到这个超级链接指向一个名为login.aspx的页面,但您还没有编写它。这又是一个用三行代码实现的Web页,因此我们继续进行试验,现在就创建它:
<form runat='server'>
<asp:login runat='server'/>
</form>
如果您曾经手工实现过窗体身份验证,您就会赞赏这三行代码。过去,执行数据库查找的等同实现需要两倍数量的代码。
现在回到您的浏览器,并单击Login链接,它将您带到如图5所示的登录页。尝试用一个无效用户名或密码登录,可以发现,系统会弹出一条适当的默认错误消息。这条消息不会给攻击者太多的信息。而一个没有经验的开发人员也决不会无意中发送回一条消息给该用户,告诉他获得了正确的用户名,请尝试猜测另一个密码!
图5 登录页
继续进行并键入一个有效的用户名和密码,这是您先前通过adduser.aspx页输入的用户名和密码,然后您应该被重定向回default.aspx页。由于您没有为登录控件提供任何自定义操作,所以默认情况下它只通过窗体身份验证来让您登录,这意味着您的浏览器现在有了一个存放用户名的加密cookie。
既然您已经重定向回default.aspx页面,您看到有什么不同吗?登录状态控件现在应该显示Logout而不是Login。因为窗体身份验证cookie是与请求一起发送的,所以FormsAuthenticationModule创建了一个经过身份验证的用户主体,并且将其与该请求的上下文相关联。登录状态控件会注意到这种情况,并且改变成允许您注销。尝试注销并重新登录来查看这项工作。
现在,让我们再添加一些代码到default.aspx页面:
<h3>User Name: <%= User.Identity.Name %></h3>
<h3>User Type: <%= User.GetType() %></h3>
刷新这个页面,会看到您用来登录的用户名。请注意,表示用户的基本对象是GenericPrincipal类型,这是FormsAuthenticationModule表示用户的方式。一旦您启动Role Manager,就会注意到这种类型变化,因为当启用时,新的RoleManagerModule就取代了由FormsAuthentication使用它自己的类型生成的用户。
现在,让我们添加一个LoginView控件到default.aspx页面,以显示可以根据用户的登录而改变的内容。使用这个控件最简单的方法是提供两个内容块:一个用于匿名请求(在用户登录之前),另一个用于身份验证请求(在用户登录之后):
<asp:loginview runat='server'>
<anonymoustemplate>
<h4>If you see this, you've not yet logged in!</h4>
</anonymoustemplate>
<loggedintemplate>
<h4>Welcome to my website, <asp:loginname runat='server'/>!</h4>
</loggedintemplate>
</asp:loginview>
当您登录或注销时,您应该看到LoginView控件中的文本发生了改变,正如我们所预料的一样。这是一个非常简单的想法,但是它确实让您的代码变得更清晰。
定义角色
我已经制作了一个简单的页面,它允许您使用Role Manager将用户添加到角色,但是在您能使用它之前,还需要为应用程序启用Role Manager。回到配置工具,并找到“Authentication”选项卡。选中标有“Role management enabled”的复选框,然后应用这个改变。
<form runat='server'>
<table>
<tr><td>Role:</td><td><asp:textbox id='role' runat='server'/></td></tr>
<tr><td>User:</td><td><asp:textbox id='user' runat='server'/></td></tr>
</table>
<asp:button text='Add user to role!' οnclick='onSubmit' runat='server'/>
<p><asp:label id='msg' runat='server' viewstateenabled='false'/></p>
</form>
<script runat='server'>
void onSubmit(object sender, EventArgs args)
{
if (!Roles.RoleExists(role.Text))
{
Roles.CreateRole(role.Text);
msg.Text = "Created a new role.";
}
Roles.AddUserToRole(user.Text, role.Text);
}
</script>
图6 addrole.aspx
在图6 中显示了addrole.aspx页面的代码,而图7显示了窗体的外观。将这个页面放到虚拟目录中并且将浏览器指向它,这样您就可以添加了一些角色。指定一个用户名(您前面通过adduser.aspx窗体添加的用户名)和一个角色名,然后按下按纽将用户添加到角色中。代码将首先添加角色(如果它不存在的话),然后将用户添加到角色。在后台,Role Manager会在Membership服务使用的同一Microsoft Access数据库中跟踪这些角色映射,但是这实际上只是巧合。Role Manager可以将其数据存储在SQL Server或任何其他的存储中,并且不必使用与Membership服务相同的机制。为此,Membership和Role Manager的提供程序模型完全不同。
图7 添加角色
如果您曾经在ASP.NET中实现了自定义角色,则一定会欣赏内置的Role Manager,因为您不再需要成为ASP.NET HTTP管道的主管,就能实现基于角色的安全性。一旦您添加了一些角色,您就可以回到default.aspx,并且可以使用LoginView控件来做一些有趣的事情。在<loggedintemplate/>元素之后添加另一个部分:
<rolegroups>
<asp:rolegroup roles='ForumModerators'>
<contenttemplate>
<h4>Controls for forum moderators go here.</h4>
</contenttemplate>
</asp:rolegroup>
<asp:rolegroup roles='Friends'>
<contenttemplate>
<h4>Welcome, friend!</h4>
</contenttemplate>
</asp:rolegroup>
</rolegroups>
您可能没有选择与我相同的角色,因此,需要用您自己的角色名来代替我的角色名,并且调整内容使之适合角色。完成后,就可以通过使用不同角色中的不同用户帐户登录来检验您的新页面,并且观察当角色改变时页面的内容如何改变。请注意,如果两个角色组都与用户的角色相匹配,则总是显示第一个匹配的角色组(从上到下)。
虽然这并不新鲜,但是请您记住,您始终可以通过User.IsInRole以编程方式测试角色。还需要谨记的是,您可以使用web.config中的部分来准许或拒绝访问各个页面,如下所示:
<authorization>
<deny users='?'/>
<allow roles='ForumModerators'/>
<deny users='*'/>
</authorization>
第一项告诉ASP.NET禁止传入任何未经身份验证的请求(强制执行身份验证)。第二项和第三项确保只有ForumModerators可以访问web.config文件所驻留的目录树中的内容。记住,授权部分可用于子目录中的web.config文件,也可以用于元素,以控制对单独文件的访问。
密码恢复
在这个介绍性演示中,我还没有向您展示密码恢复控件,因为对它的使用需要进行仔细的考虑。您可能知道这个控件的作用:它可让用户请求通过电子邮件将其密码发送给自己。在决定将明文密码通过电子邮件发送给用户之前,您需要做一些风险评估。
事实上,如果您将这个控件放在现有站点的一个页面上,它将不会起作用,因为在默认情况下,Membership服务会拒绝公开明文密码。即使它想这样做也是不可能的,因为在默认情况下它只存储密码的单向哈希值而不存储密码本身。当要求验证密码时,Membership服务会哈希所提交的密码,并将该哈希值与其副本相比较。如果您想要恢复明文密码,则可以将Membership提供程序重新配置为以加密的形式存储密码,在这种情况下,Membership提供程序将使用来加密密码。这样就可以对密码进行解密并通过电子邮件发送给用户。
如果您存储哈希密码(这真是一个好主意),就需要准备一种替换的方法来对用户进行身份验证。您不能通过电子邮件将密码发送给用户,但是如果您提前问了几个问题,比如“您最喜欢的宠物叫什么名字?”,您就可以使用这些答案来对用户进行身份验证,并允许他向您发送一个新的密码。然而,Membership服务并不支持为每个用户保留问题和答案,使用它仅仅是为了决定是否可以通过电子邮件发送密码,因此它不能与哈希密码一起使用。据我看来,这方面将耗费一些工作。
在Building Secure Software (Addison-Wesley, 2002)的第95页上,Viega和McGraw提出了一种通过问与答来重新设置密码的好模型。这种模型需要使用数百个问题的集合,当用户首次设置她的帐户时,它会随机挑出一组问题来询问用户。如果用户请求重新设置密码,您就可以问她这其中的一些问题。这需要她正确的回答许多质询问题以便继续进行操作。如果用户成功地回答了所有问题,您还可以选择一组新的随机问题代替前面使用的问题来进行提问。
调整提供程序
到目前为止,我特意使用默认设置来保持它的简单,但是您需要调整这些设置以适合您自己的环境。例如,如果您想让Membership服务将其数据存储在SQL Server中,您就应该选择AspNetSqlProvider而不是默认的AspNetAccessProvider。这个设置位于配置工具的Authentication页面。
但是如果您已经有了一个需要集成的现有用户数据库,该怎么办?它肯定不会有AspNetSqlProvider需要的表和列。另外,如果它在AS/400服务器或Oracle安装上,又该怎么办?幸运的是,Membership和Role Manager系统都构建在分层模型上,我已经在图8中显示了这个模型。您可以通过扩展定义在System.Web.Security命名空间中的抽象MembershipProvider类来完全代替Membership数据存储。同样,您可以通过扩展RoleProvider来代替Role Manager数据存储。Rob Howard在他的“'Nothin' But ASP.NET”专栏中详细讨论了提供程序模型。
图8 提供程序模型
当然,使用现有的提供程序是最简单的方法。在最初测试版中,有两个模型。一个与Access数据库协同工作,正如您所看到的,它运行得超常的好。另一个是我先前提到的SQL Server提供程序。到测试版时,应该还有针对Active Directory来验证用户的Membership提供程序,以及从Authorization Manager中查找角色的Role提供程序。
即使您选择了一个内置提供程序,您还可以在web.config中调整它的行为。图9显示了SQL Server Membership提供程序的提供程序设置。请注意passwordFormat设置,其中,您可以在三个选项之间进行选择:Hashed(默认)、Encrypted和Clear。然后,您可以通过enablePasswordRetrieval和requiresQuestionAndAnswer属性来选择密码恢复策略。当然,假如您选择使用哈希密码,您就必须将enablePasswordRetrieval设置为false。或者,您可以在系统通过电子邮件将密码发给用户之前,要求用户回答一个质询问题。
图9 提供程序设置
数据库的连接字符串并不存储在web.config文件中,而是通过间接方式引用。请注意,该属性称为connectionStringName,并且指向专门设计用来存放连接字符串的machine.config部分。将连接字符串存储在web.config文件之外是一个好方法,特别是在您无法使用集成身份验证并强制使用密码的情况下。ASP.NET 2.0被设计为支持配置文件敏感部分的XML加密,这对于machine.config中的连接字符串部分而言,确实是一项非常便利的功能。
可以将Role Manager配置为使用cookie或URL munging,并可以在cookie中缓存角色,以减少到角色数据库的往返行程。该缓存是智能的:如果缓存角色的数目开始变得很大,Role Manager将在cookie中缓存最近使用的角色,而动态地查找使用最少的角色。这种功能可能是由于需要用有限的存储空间来支持移动设备而激发的。
还可以调整许多其他的设置,但是我准备把它们留给您自己去研究。同时,让我们看一看可用来调整前面使用的服务器端安全控件的方法。
控件调整
用三行代码就能创建一个登录页是十分简洁的,但是一般来说,您需要对登录控件进行一些自定义以适合您的应用程序。图10 显示了一些代码,您可以用这些代码来代替前面创建的简单登录页。另外,您还可以用您希望Web控件所具有的所有属性来修改这些控件的外观。而通过ASP.NET 2.0中的主题支持,您不必更改代码就可以在整个Web站点中保持一致的外观。
<form runat='server'>
<asp:login runat='server'
titletext='ACME.COM Security'
instructiontext='You must log in to continue'
usernamelabeltext='Email:'
submitbuttontext='Continue'
createusertext='register'
createuserurl='adduser.aspx'
displayrememberme='false'
failuretext='Leave now, hacker!'
helppagetext='help'
helppageurl='http://www.success.com'
/>
</form>
图10 自定义登录控件
登录控件的一个有趣的功能是,它不必像我在这个例子中所做的那样固定在自己的页面上。相反,您可以将其作为主页面的一部分,这样它就会始终出现在页边空白处。一旦用户登录,实际上您就不想再看到它,因此在默认情况下,当它检测到经过身份验证的用户已经存在时,就会自动消失。您可以通过VisibleWhenLoggedIn属性来调整这种行为。这是开发人员通过ASP.NET 1.1手动实现该功能的一个例子,现在它内置在ASP.NET 2.0中。
其他控件也有类似的选项。例如,如果您希望对登录或注销的用户显示一个漂亮的按钮,请设置登录状态控件上的Login(Out)ImageUrl属性。
要体验它是如何工作的,请使用Visual Studio 2005项目向导来创建一个Internet Web站点。对于本文,只有在您将“Web.vssettings”IDE设置文件导入Visual Studio的情况下才会显示这个向导。您可以通过Tools-Import/Export Settings对话框来做到这一点。该向导包括到此为止所谈到的全部功能,并且提供了丰富的UI自定义,以获得您希望新Web站点具有的外观和功能。
成员和角色编程
当您希望远离服务器端安全控件时,最好知道您也可以直接使用实现这个高级功能的类。为了学习这些服务的编程模型,您需要分析两个主类:Membership和Roles。由于文章的篇幅所限,我无法在这里详尽地介绍它们,而在产品向最终版本发展的过程中,其中的一些细节肯定会改变,不过,让我首先带您体验一下。
从Membership类中,您可以创建和管理用户,其中每一个用户都是由MembershipUser类的一个实例表示的。这个类表示用户配置文件,包括诸如Email、CreationDate、PasswordQuestion等属性。在创建和更新这些用户配置文件时,您可以通过Membership类来这样做,因为它是分层模型,隐藏了存储配置文件的位置和方式的细节(请参见图8)。此类提供了更改用户密码和将密码重新设置为一个计算机生成的随机密码的方法,这是一个时间戳,它跟踪用户的活动,从而维护当前用户的数量(您可以通过调用Membership类中的GetNumberOfUsersOnline方法来获取这个数目)。
要验证一个用户密码,只需调用Membership类中的ValidateUser方法并传入用户名和密码就可以了。基本提供程序将负责所有必要的密码哈希和解密。如果用户忘记他的用户名,可以通过要求他提供一个电子邮件地址并将其传送给GetUserNameByEmail的方法来提醒他,但这不是一个安全的选择。
无cookie的窗体身份验证
当我教授ASP.NET窗体身份验证时遇到最多的抱怨之一就是它需要cookie。幸运的是,在ASP.NET 2.0中已经取消了该限制。现在,web.config中的元素上有一个新的“cookieless”属性。您可以将此属性设置为以下四个值之一:UseCookies、UseUri、UseDeviceProfile或AutoDetect。
UseCookies和UseUri分别强制要求FormsAuthenticationModule对所有的请求使用cookie或者URL munging。UseDeviceProfile用于查看浏览器功能,以确定使用哪种模式。最后,AutoDetect将尝试设置cookie,如果失败,就将改为使用URL munging。典型的URL在保护之后如下所示(省略号是我添加的,因为这些URL可能很长):http://www.acme.com/foo/(F(Cvc...A1))/default.aspx。
URL中括号内的部分包含cookie通常包含的数据,它将被HTTP管道中的模块取消,因此如果您从ASPX页读取Request.Path属性,将不会在URL中看到任何多余的内容。如果您重定向请求,URL将被自动保护。换句话说,这段代码将(正确地)带您回到您当前正在查看的页面(在URL被正确保护的情况下):
Response.Redirect(Request.Path)
这种功能应该使窗体身份验证在很大程度上得以更广泛地实现。然而,随着使用ASP.NET窗体身份验证的Web站点不断增多,越来越多的攻击者会试图发现弱点,因此遵守一些基本规则是一个好主意。
一些预防措施
如果不通过安全套接字层(SSL)进行保护,窗体身份验证将不会很强大。至少应该通过安全连接将您的登录页发送给用户并传送回Web服务器,以防止窃听者窃取用户的明文密码。但通常这样做并不够。由于cookie工作方式的缘故,窃取窗体身份验证cookie的窃贼如果已经窃取了登录信息,那么将无法实现重播检测。记住,cookie通常与每个请求一起发送,甚至对于像请求页面按钮的GIF文件这样简单的事情也是如此。一旦被盗,攻击者就可以使用该cookie来模仿用户。为了减少这种风险,您需要大大缩短cookie的超时设置,或者通过SSL运行Web站点的整个部分(或者,最好是整个网络)。
对于需要高安全性的站点,我更愿意选择后者,当人们抱怨SSL运行缓慢时,我会问他们为什么不去购买硬件来加速它。然而,一些公司仅仅在部分站点中坚持使用SSL。如果您是这种情况,则可以通过启用元素中的requireSSL属性来减少这些cookie重播攻击。这会将“Secure”属性添加到窗体身份验证cookie,它指示浏览器应该仅通过安全通道将cookie发送回服务器。换句话说,它不会与未通过SSL运行的请求一起发送。此功能已添加到.NET Framework 1.1版中,因而不是ASP.NET 2.0所特有的。ASP.NET 2.0中的新特性是,这种对策还可以应用于会话cookie:
<httpCookies requireSSL='true' httpOnlyCookies='true'/>
由于安全的cookie不会与未通过SSL运行的请求一起发送,所以对于可以通过原始HTTP进行访问的页面,您可以肯定User.Identity.IsAuthenticated每次都会返回false。换句话说,您将不知道那些在任意页面上未通过SSL运行的用户是谁。请注意,即使您决定通过SSL来运行整个站点,在您偶然允许通过原始HTTP访问一两个文件的情况下,启用requireSSL属性确实是一个好主意。
作为防止跨站点脚本攻击的措施,httpOnlyCookies属性是非常有用的;它指示浏览器不应该从脚本访问cookie。它使用一个名为HttpOnly的cookie属性,目前只有新版的Internet Explorer才能识别它,但这是个很好的主意,我希望其他的浏览器厂商采用它。
小结
ASP.NET 2.0为使用窗体身份验证的Web站点提供了重要的安全性优势。通过提供用户配置文件储备库以及对角色的支持,窗体身份验证将走出ASP.NET内行的视野,而得以更广泛地实现。现在对于我来说,回过头来使用旧版本还真难!
作者简介:Keith Brown是专门研究应用程序安全性的独立顾问。Keith定期为DevelopMentor授课,其中,他负责安全性方面的课程。他与人合著了Programming Windows Security(Addison-Wesley,2000年)一书,并且正在编写一本有关.NET安全性的新书。要在线阅读,请访问 http://www.pluralsight.com/keith。