在程序中使用表单验证基本上需要完成以下步骤:
cookie 通过一个机器特定的密钥来加密,这个密钥在 machine.config 中定义。通常这个密钥的内容并不重要,但是在一个 Web 集群中,你需要保证所有的服务器使用同样的密钥,这样一台服务器才可以解密另一台服务器创建的 cookie。
必须在 web.config 文件中正确的配置表单验证。每一个 web.config 文件都有一个 <authentication> 配置节,将其 mode 特性设为 Forms 即可:
<authentication mode="Forms">
<!-- Detailed configuration options -->
</authentication>
<authentication> 配置节在程序的 web.config 文件中必须位于最高级别。如果 mode 特性被设为 Forms,ASP.NET 会加载并激活 FormsAuthenticationModule,它将为你完成大部分工作。上面的配置使用表单验证的默认配置,你可以通过在 machine.config 中的 <system.web> 节中添加设置覆盖这些默认设置。
你也可以通过在 <authentication> 的子标签 <forms> 中指定额外的设置来在程序中覆盖这些默认设置:
<authentication mode="Forms">
<forms
name="MyCookieName"
loginUrl="DbLogin.aspx"
timeout="20"
slidingExpiration="true"
cookieless="AutoDetect"
protection="All"
requireSSL="false"
enableCrossAppRedirects="false"
defaultUrl="MyDefault.aspx"
domain="www.mydomain.com"
path="/" />
</authentication>
这里,domain 属性被设置为一个代表你的域名的值。不过,在开发时通常会使用 URL http://localhost:<port>,此时,用于访问应用程序的 URL 和真实的域名并不相同,因此,表单验证将不可行。因为它把用于访问 WEB 服务器的 URL 和域名 cookie 的名称相匹配。
下表列出了大多数情况下会用到的属性:
选 项 |
默认值 |
描 述 |
name | .ASPXAUTH | 验证所使用的 HTTP cookie 的名字。多个应用程序应设置不同名字 |
loginUrl | login.aspx | 设置将用户跳转到哪一个页面完成登录 |
timeout | 30 | cookie 过期时间。如果经常过期,用户将不得不多次登录,程序可用性下降。如果从不过期,将面临 cookie被盗窃滥用的危险 |
slidingExpiration | true | 激活或禁止验证 cookie 的可变过期时间。如果激活它,意味着每次请求都会使 cookie 的过期时间复位 |
cookieless | UseDeviceProfile | 指定运行时是否用 cookie 将表单验证票据发送到客户端。可能的选项有 AutoDetect、UseCookies、UseUri、UseDeviceProfile |
protection | All | 指定 cookie 的保护级别。All 将对所有 cookie 加密并签名。可选的有 None、Encryption(仅加密)、Validation(仅签名) |
requireSSL | false | 如果设为true,在web服务器没有激活SSL时,浏览器不会发送 cookie,表单验证将不起作用 |
enableCrossAppRedirects | false | 激活跨应用程序重定向。这只有两个应用程序都依赖同一个凭证存储并使用相同的用户和角色集合时才起作用 |
defaultUrl | default.aspx | 如果用户登录成功,它包含了原来请求页面的 URL 信息 |
domain | <empty string> | 指定 cookie 在哪个域中有效。 |
path | / | 应用程序所处理的 cookie 的路径。推荐使用默认值。 |
web.config 中的凭证存储
你可以选择将用户凭证存放在什么地方,一个自定义文件或者数据库中或任何地方,只要你提供代码利用你的凭证存储验证用户输入的用户名和密码。
最简单的存储就是将用户凭证直接存储在 web.config 文件中,使用 <forms> 的子标签 <credentials> 中:
<authentication mode="Forms">
<forms
name="MyCookieName"
loginUrl="DbLogin.aspx"
timeout="20">
<credentials passwordFormat="Clear">
<user name="Admin" password="(Admin1)"/>
<user name="skysoot" password="111111"/>
</credentials>
</forms>
</authentication>
首先,使用 web.config 作为凭证存储只适用于很少用户的简单解决方案。在较大的场景中,应使用成员资格 API。其次,你可以散列化存储在 web.config 文件中的凭证和密码。散列化是对密码实施单向加密,这意味着密码将不能再被解密。
为了演示表单验证的重定向功能,现在开始,将使用拒绝所有非验证用户访问的简单技术。为此,配置 web.config,设置 <authorization> 元素添加一个新的授权规则,如下:
<authorization>
<deny users="?"/>
</authorization>
问号?是一个通配符,它代表所有的匿名用户。这样你拒绝了所有的匿名用户访问。每一个用户都必须被验证,每一次用户请求都需要表单验证票据(cookie)。
不像<authentication>元素,<authorization>元素并没有被限制在 Web 应用程序根目录下面的 web.config 文件中。你可以在任何子目录中使用它,这样你就可以为不同组的页面设置不同的授权规则了。
现在,创建一个简单的自定义登录页面,此外,还必须拥有验证凭证的代码。如下图:
注意,浏览器地址中的 URL 包含了原来请求的页面并将其作为一个查询参数。这个参数在后面会被 FormsAuthentication 类用来重定向到原来请求的页面:
登录界面的页面代码如下:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="DbLogin.aspx.cs" Inherits="DbLogin" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Untitled Page</title>
</head>
<body>
<form id="form1" runat="server">
<div style="text-align:left">
Please Log into the System<br />
<asp:Panel ID="MainPanel" runat="server" Width="380px" BorderColor="Silver" BorderStyle="Solid"
BorderWidth="1px">
<br />
<table width="100%" border="0" cellpadding="0" cellspacing="0">
<tr>
<td width="30%" style="height: 43px">
User Name:
</td>
<td width="70%" style="height: 43px">
<asp:TextBox ID="UsernameText" runat="server" Width="80%" />
<asp:RequiredFieldValidator ID="UsernameRequiredValidator" runat="server" ErrorMessage="*"
ControlToValidate="UsernameText" />
<br />
<asp:RegularExpressionValidator ID="UsernameValidator" runat="server" ControlToValidate="UsernameText"
ErrorMessage="Invalid username" ValidationExpression="[\w| ]*" />
</td>
</tr>
<tr>
<td width="30%" style="height: 26px">
Password:
</td>
<td width="70%" style="height: 26px">
<asp:TextBox ID="PasswordText" runat="server" Width="80%" TextMode="Password" />
<asp:RequiredFieldValidator ID="PwdRequiredValidator" runat="server" ErrorMessage="*"
ControlToValidate="PasswordText" />
<br />
<asp:RegularExpressionValidator ID="PwdValidator" runat="server" ControlToValidate="PasswordText"
ErrorMessage="Invalid password" ValidationExpression='[\w| !"§$%&/()=\-?\*]*' />
</td>
</tr>
</table>
<br />
<asp:Button ID="LoginAction" runat="server" OnClick="LoginAction_Click" Text="Login" /><br />
<asp:Label ID="LegendStatus" runat="server" EnableViewState="false" Text="" /></asp:Panel>
</div>
</form>
</body>
</html>
可以看到,我们加入了验证控件,这点特别重要。绝不要相信用户的输入,它们可以保证用户只能输入有效的值。这里略微解释下正则表达式的含义:
在页面的后台代码中,我们应该编写验证用户凭证的代码。这里使用了 web.config 的凭证存储,所以验证代码非常简单:
protected void LoginAction_Click(object sender, EventArgs e)
{
// Validate(): 指示页面所有验证控件验证指派给它们的信息
Page.Validate();
if (!Page.IsValid)
{
return;
}
// FormsAuthentication: Web 应用程序管理 Forms 身份验证服务
// Authenticate(): 对照存储在应用程序配置文件中的凭据来验证用户名和密码
if (FormsAuthentication.Authenticate(UsernameText.Text,PasswordText.Text))
{
// Create the ticket, add the cookie to the response,
// and redirect to the originally requested page.
// RedirectFromLoginPage(): 将经过身份验证的用户重定向回最初请求的 URL 或默认 URL
FormsAuthentication.RedirectFromLoginPage(UsernameText.Text, false);
}
else
{
// User name or password are not correct.
LegendStatus.Text = "Invalid username or password!";
}
}
RedirectFromLoginPage() 执行了若干任务:
1. 退出系统
退出表单验证非常容易:
protected void SignOutAction_Click(object sender, EventArgs e)
{
FormsAuthentication.SignOut();
FormsAuthentication.RedirectToLoginPage();
}
2. web.config 中的散列码
表单验证具有将密码存为不同格式的功能。在 <forms> 元素的 <credentials> 配置节中,密码的格式通过 passwordFormat 特性来定义,它有 3 种有效值:
当使用散列化密码时,必须写一个工具或代码来将密码散列化,这可以是 web 程序本身的管理模块,也可以是管理 web 程序的 windows 程序。应当使用如下形式来生成密码,而不是传递纯文本的密码:
// HashPasswordForStoringInConfigFile(): 根据指定的密码和哈希算法生成一个
// 适合于存储在配置文件中的哈希密码
string hashedPwd = FormsAuthentication.HashPasswordForStoringInConfigFile(txtPwd.Text, "SHA1");
返回的散列后的密码你可以指定存储在 web.config 文件中或是你自己的用户数据库中。
但如果你想编辑存储在 web.config 文件中的用户,必须使用 .NET Framework 的配置 API。你无法通过基于 Web 的配置工具来修改这一部分。下面的代码片段展示了如何通过配置 API 来修改。这段代码只能被管理员使用:
Configuration MyConfig = WebConfigurationManager.OpenWebConfiguration("./");
ConfigurationSectionGroup SystemWeb = MyConfig.SectionGroups["system.web"];
AuthenticationSection AuthSec = SystemWeb.Sections["authentication"] as AuthenticationSection;
AuthSec.Forms.Credentials.Users.Add(new FormsAuthenticationUser(userName, userPwd));
MyConfig.Save();
只有授权用户,比如网站管理员,才被允许执行上面的代码,还必须拥有对 web.config 文件的写权限。同时,这样的代码不应该出现在 WEB 程序中,应该只把它包含在管理程序中。
3. 无 cookie 的表单验证
ASP.NET 运行时支持不使用 cookie 的表单验证。你可以通过 <authentication> 节中 <forms> 标签的特性进行配置:
<authentication mode="Forms">
<forms
name="MyCookieName"
loginUrl="DbLogin.aspx"
cookieless="AutoDetect"
...>
</forms>
</authentication>
cookieless 可能的值如下表:
UseCookies | 强制运行时用表单验证工作时使用 cookie,客户端浏览器必须支持 cookie |
UseUri | 不使用 cookie,运行时会把表单验证票据编码到请求 URL 中,且 ASP.NET 架构会处理这个 URL 中的特定部分来建立安全上下文 |
AutoDetect | 灵活的综合了上述的两种情况。支持 cookie 就用 cookie,否则用 URL |
UseDeviceProfile | 使用哪种方式取决于存储在 Web 服务器上的一个设备用户配置,这些用户配置被放在了 C:\[WinDir]\Microsoft.NET\Framework\[Version]\Config\Browsers 目录下的 .Browser 文件中 |
将凭证存储在 web.config 文件中只对简单环境有用,且一般也不会使用 web.config,原因很多:
因此,通常你会使用自定义存储来保存用户凭据,而且通常是一个数据库。下面的示例假定你已经写了一个函数 MyAuthenticate() 用来连接到数据库检验用户名和密码是否匹配:
protected void LoginAction_Click(object sender, EventArgs e)
{
Page.Validate();
if (!Page.IsValid) return;
if (this.MyAuthenticate(UsernameText.Text, PasswordText.Text))
{
FormsAuthentication.RedirectFromLoginPage(UsernameText.Text, false);
}
else
{
LegendStatus.Text = "Invalid username or password!";
}
}
ASP.NET 提供了一个现成的框架和一系列与安全相关的控件来帮助完成这些工作。成员资格 API 包含了一个基于 SQL Server 的数据存储机制。这些技术在我后续的文章中会一一阐述。
至目前为止,所有的例子都是使用非持久化的验证 cookie 来维持验证票据的,这意味着只要关闭浏览器,cookie 会立刻被删除,这对保证安全来讲很有意义,非持久化的 cookie 也使“会话劫持攻击”更加困难并受到更多限制。
持久验证 cookie 会增加安全风险,但它们适用于一些特定场合。如果是为了个性化而不是为了控制对受限资源的访问,可以肯定用户每次访问无需登录带来的便利比考虑非授权使用所带来的风险更重要。
实现持久化 cookie 非常简单,只需要将 RedirectFromLoginPage() 方法或者 SetAuthCookie() 方法的第二个参数设为 true 即可:
FormsAuthentication.RedirectFromLoginPage(UsernameText.Text, true);
浏览器关闭时,持久化 cookie 不会过期。但就像非持久化 cookie 一样,当调用 FormsAuthentication.SignOut() 或者到了 <forms> 元素的 timeout 特性设定的超市时间后(默认30分钟),它们就会过期。这一点引发了一个潜在的矛盾。你或许想让用户选择使用短期的非持久化 cookie 还是存储一个长期的持久化 cookie。然而,你只能将 timeout 特性设为一个单一值。解决办法是使用 FormsAuthentication.GetAuthCookie() 方法来创建持久化 cookie,设置过期时间和日期,然后自己将这个 cookie 写到 HTTP 响应流中。
下面的示例创建了一个持久化 cookie,又执行了一些额外的步骤来设置 cookie 的生命周期为 10 天:
protected void LoginAction_Click(object sender, EventArgs e)
{
Page.Validate();
if (!Page.IsValid) return;
if (FormsAuthentication.Authenticate(UsernameText.Text, PasswordText.Text))
{
HttpCookie cookie = FormsAuthentication.GetAuthCookie(UsernameText.Text, true);
cookie.Expires = DateTime.Now.AddDays(10);
Response.Cookies.Add(cookie);
Response.Redirect(FormsAuthentication.GetRedirectUrl(UsernameText.Text,true));
}
else
{
LegendStatus.Text = "Invalid username or password!";
}
}
GetAuthCookie() 方法创建了一个 HttpCookie 的新实例,并用 Response 手动写入到响应流中去。