理解和实现 ASP.NET 自定义表单身份验证






4.87/5 (67投票s)
理解和实现 ASP.NET 自定义表单身份验证。
引言
本文讨论 ASP.NET 的自定义表单身份验证机制。我们将了解如何将自定义数据库与 ASP.NET 表单身份验证机制结合使用,为我们的应用程序提供所需的安全性级别。
背景
我们已经了解了如何使用 ASP.NET 的角色和成员资格类来为我们的应用程序提供身份验证和授权 (参考此文)。此外,我们也了解了 ASP.NET 提供的服务器控件,可以方便地在我们的应用程序中实现角色和成员资格功能。
当我们希望在应用程序中提供身份验证和授权时,这些角色和成员资格类非常有用。ASP.NET 还提供了一种实现自定义角色和成员资格的方法,以便更精细地控制各项功能。
我们仍然可能遇到需要拥有自己的数据库来跟踪用户及其角色的情况。原因可能包括:
- 我们有一个现有数据库,并且正在尝试使用它实现一个应用程序。
- 角色和成员资格功能对于我们的应用程序来说是多余的。
- 角色和成员资格功能不足以满足我们的应用程序需求,我们需要自定义数据。
在以上所有场景中,我们发现需要使用 ASP.NET 表单身份验证,但需要根据我们的数据库架构和我们自定义编写的用户管理代码。这正是 ASP.NET 的强大之处。ASP.NET 为我们提供了功能齐全且强大的身份验证和授权功能,同时还允许我们完全控制和高度自定义这些功能(实际上,这不仅仅局限于身份验证相关的内容,几乎所有 ASP.NET 的领域都是如此)。
因此,通过拥有自定义的表单身份验证机制,我们可以轻松实现我们的目标。使用自定义的表单身份验证机制,我们可以拥有自己的表来管理用户,并使用现有的表单身份验证机制,结合 `GenericPrincipal`。让我们通过一个示例来工作,看看如何实现这一点。
使用代码
让我们先明确我们的需求。我们将实现一个具有以下结构的网站:
顶层的默认页面和登录页面可以被任何人访问。其他文件夹中的页面只能由相应角色的用户访问。为了更清晰地理解,让我们看一下 `PlatinumUser` 文件夹的 `web.config`。
<configuration>
<appSettings/>
<connectionStrings/>
<system.web>
<authorization>
<allow roles="platinum" />
<deny users="*" />
</authorization>
</system.web>
</configuration>
以及我们用于用户管理的数据库,如下所示:
我们有一个用户表,每个用户可以拥有多个角色。我们将使用此表来验证用户。
注意: 该数据库既未经过优化也未经过规范化,因为这不是本文的主要目的。实际世界中的数据库示例将更加优化,并且可能更复杂。密码肯定不会以明文存储。
在继续之前,让我们先看一下我们将要遵循的用于实现自定义表单身份验证并达到预期功能的小算法。
- 配置应用程序以使用表单身份验证。
- 创建一个登录页面。
- 每当用户尝试访问受限制区域时,将其重定向到登录页面。
- 当用户尝试登录时,使用数据库验证其凭据。
- 如果登录成功,将用户名及其角色保存在 Session 变量中以供后续使用。
- 为用户创建一个身份验证票证(一个加密的 Cookie)。此票证可以是持久的或非持久的。
- 通过身份验证票证提取用户/角色。
- 使用上一步找到的用户/角色创建一个 `Principal` 对象,以便 ASP.NET 表单身份验证机制可以使用这些数据。
注意: 此算法中的每个要点都将在下面的解释中得到强调。
我们已经制定了算法。现在我们将看看每个步骤是如何完成的。让我们从“配置应用程序以使用表单身份验证”开始。为此,我们需要在 `web.config` 文件中设置身份验证模式。
<authentication mode="Forms">
<forms loginUrl="Login.aspx" name="MyCustomAuthentication" timeout="30"/>
</authentication>
此 `web.config` 指示该应用程序将使用 `Forms` 身份验证。默认的 `loginUrl` 是 `Login.aspx`。身份验证票证 Cookie 的名称将是 `MyCustomAuthentication`,如果此 Cookie 以非持久模式创建,则其 `timeout` 周期为 30 分钟。
现在,我们的网站已设置为使用表单身份验证,并且我们还指定了默认登录页面,因此我们还处理了算法的第 3 点,即:每当用户尝试访问受限制区域时,将其重定向到登录页面。接下来是创建登录页面。
完成此登录页面后,我们需要一种机制来“当用户尝试登录时,使用数据库验证其凭据”。为此,我创建了一个小型辅助类。理想情况下,此逻辑将放置在数据访问层或可能存在于 ORM 中。但为了简单起见,我创建了一个小型辅助类来完成此任务。
public class DBHelper
{
//Validate the user from DB
public static bool CheckUser(string username, string password)
{
DataTable result = null;
try
{
using (SqlConnection con = new SqlConnection(ConfigurationManager.ConnectionStrings["userDbConnectionString"].ConnectionString))
{
using (SqlCommand cmd = con.CreateCommand())
{
cmd.CommandType = CommandType.Text;
cmd.CommandText = "select password from Users where username = @uname";
cmd.Parameters.Add(new SqlParameter("@uname", username));
using (SqlDataAdapter da = new SqlDataAdapter(cmd))
{
result = new DataTable();
da.Fill(result);
}
if (password.Trim() == result.Rows[0]["password"].ToString().Trim())
{
//user id found and password is matched too so lets do soemthing now
return true;
}
}
}
}
catch (Exception ex)
{
//Pokemon exception handling
}
//user id not found, lets treat him as a guest
return false;
}
//Get the Roles for this particular user
public static string GetUserRoles(string username)
{
DataTable result = null;
try
{
using (SqlConnection con = new SqlConnection(ConfigurationManager.ConnectionStrings["userDbConnectionString"].ConnectionString))
{
using (SqlCommand cmd = con.CreateCommand())
{
cmd.CommandType = CommandType.Text;
cmd.CommandText = "select roles from Users where username = @uname";
cmd.Parameters.Add(new SqlParameter("@uname", username));
using (SqlDataAdapter da = new SqlDataAdapter(cmd))
{
result = new DataTable();
da.Fill(result);
}
if(result.Rows.Count == 1)
{
return result.Rows[0]["roles"].ToString().Trim();
}
}
}
}
catch (Exception ex)
{
//Pokemon exception handling
}
//user id not found, lets treat him as a guest
return "guest";
}
}
现在,每当用户尝试登录时,“如果登录成功,将用户名及其角色保存在 Session 变量中以供后续使用”,并且我们还需要“为用户创建一个身份验证票证(一个加密的 Cookie)”。因此,让我们在登录页面的按钮 `click` 事件中完成所有这些操作。
protected void Button1_Click(object sender, EventArgs e)
{
string roles;
string username = TextBox1.Text.Trim();
if (DBHelper.CheckUser(username, TextBox2.Text.Trim()) == true)
{
//These session values are just for demo purpose to show the user details on master page
Session["User"] = username;
roles = DBHelper.GetUserRoles(username);
Session["Roles"] = roles;
//Let us now set the authentication cookie so that we can use that later.
FormsAuthentication.SetAuthCookie(username, false);
//Login successful lets put him to requested page
string returnUrl = Request.QueryString["ReturnUrl"] as string;
if (returnUrl != null)
{
Response.Redirect(returnUrl);
}
else
{
//no return URL specified so lets kick him to home page
Response.Redirect("Default.aspx");
}
}
else
{
Label1.Text = "Login Failed";
}
}
在继续之前,有一点需要理解。当使用表单身份验证时,每当需要身份验证时,ASP.NET 框架都会检查当前的 `IPrinciple` 类型对象。此 `IPrinciple` 类型对象中包含的用户 ID 和角色将决定用户是否被允许访问。
到目前为止,我们还没有编写代码将用户特定详细信息推送到此 `Principal`。要做到这一点,我们需要在 `global.asax` 中重写一个名为 `FormsAuthentication_OnAuthenticate` 的方法。每次 ASP.NET 框架尝试检查与当前 `Principal` 相关的身份验证和授权时,都会调用此方法。
现在我们需要重写此方法。检查身份验证票证(因为用户已经被验证并且票证已被创建),然后将此用户/角色信息提供给 `IPrinciple` 类型对象。我们可以实现我们自己的自定义 `Principal` 类型,但为了保持简单,我们将创建一个 `GenericPriciple` 对象并将我们的用户特定详细信息设置到其中(覆盖了我们算法的第 7 点和第 8 点)。
protected void FormsAuthentication_OnAuthenticate(Object sender, FormsAuthenticationEventArgs e)
{
if (FormsAuthentication.CookiesSupported == true)
{
if (Request.Cookies[FormsAuthentication.FormsCookieName] != null)
{
try
{
//let us take out the username now
string username = FormsAuthentication.Decrypt(Request.Cookies[FormsAuthentication.FormsCookieName].Value).Name;
//let us extract the roles from our own custom cookie
string roles = DBHelper.GetUserRoles(username);
//Let us set the Pricipal with our user specific details
e.User = new System.Security.Principal.GenericPrincipal(
new System.Security.Principal.GenericIdentity(username, "Forms"), roles.Split(';'));
}
catch (Exception)
{
//somehting went wrong
}
}
}
}
现在,当我们运行应用程序时,我们将看到授权机制正在按各自 `web.config` 文件中的指定方式工作,与我们自己的自定义数据库和身份验证逻辑完美结合。
注意: 强烈建议运行示例应用程序、查看代码和调试应用程序,以全面理解上述概念。
关注点
当我遇到自定义表单身份验证的需求时,我有点不知所措。部分原因是我只知道 ASP.NET 默认的成员资格提供程序是如何工作的,部分原因是关于自定义表单身份验证有多难的传言。当我坐下来研究它时,我发现它相当直接和简单。
还有一个重要的领域值得深入研究,以完整理解身份验证和授权机制,那就是实现自定义成员资格提供程序。也许我会在另一篇文章中介绍它。
历史
- 2012 年 6 月 20 日:初版。