表单身份验证和基于角色的授权:一种更快、更简单、更正确的方法






4.78/5 (118投票s)
本文介绍了一种在 ASP.NET 中实现基于角色的授权与表单身份验证的正确且更智能的方法。
- 下载表单授权演示(使用“RoleBasedFormsAuthentication”类库的示例网站)- 12.71 KB
- 下载 RoleBasedFormsAuthentication(一个实现基于角色授权的类库)- 13.5 KB
问题空间
令人遗憾但事实是,“ASP.NET 中的表单身份验证不直接支持基于角色的授权”。如果您已经实现了表单身份验证,并配置了 `web.config` 中“用户”和“角色”的授权规则,您会发现“用户”的访问规则工作正常,但“角色”的访问规则根本不起作用。您可能认为,在著名的 `FormsAuthentication.RedirectFromLoginPage()` 或其他方法中一定有办法指定用户角色。但事实并非如此!
背景
这真的很令人惊讶,因为在现实生活中,大多数(如果不是全部)应用程序实际上需要基于用户角色而不是用户名的系统资源授权。所以,如果您将在即将开发的 ASP.NET 应用程序中使用表单身份验证,并且需要在系统中实现基于角色的授权,那么您就遇到了一个问题。
等等,这并非完全正确,原因有两个
原因 1:自 ASP.NET 2.0 起,我们有了 Membership(成员资格)。它包括 Membership(用户)服务、Role(角色)服务和 Profile(用户属性)服务。通过使用 Membership,您可以轻松地在 ASP.NET 应用程序中实现基于角色的授权。
原因 2:即使您不使用 Membership,也可以编写一些代码来实现表单身份验证中的基于角色的授权。基本上,您需要在对用户进行身份验证后,自己创建身份验证票证并将用户角色推送到“UserData
”属性中。同时,您还需要在后续请求中从身份验证票证的同一“UserData
”属性中检索用户角色,并将其设置到当前的 `User` 属性中。这个技巧是有效的,很多人已经这样做了。
那么,本文是关于什么的?
嗯,本文假设您由于一些好的原因,直接使用了表单身份验证而不是 ASP.NET Membership。因此,您按照网上许多文章(例如 这篇)的建议实现了基于角色的授权。但我告诉您,您很可能最终进行了不正确且不完整的实现,并且在不久的将来可能会遇到问题。
本文将解决建议的实现方法存在的问题,并为您提供一种正确、智能、快速的基于角色的授权实现方式,前提是您没有在系统中进行 ASP.NET Membership。您只需要 5 分钟即可完成实现!
如果您是 ASP.NET 新手,并且对表单身份验证感到困惑,请在继续之前查看 这篇文章。
好的,那么建议的方法有什么问题?
如前所述,建议的基于角色的授权实现方法存在一些问题,我在我的一款 ASP.NET 应用程序中尝试实现它们时发现了这些问题。我按照其中一篇文章的建议操作,发现授权工作正常。但是,为了满足客户的要求,我不得不增加 `
我很好奇地调查了这个问题,并发现了另一个问题。我设置了 `
此外,我快速查看了(为实现基于角色的授权而编写的)身份验证/授权代码,并想,为什么我需要编写所有这些代码?任何人都可以通过更改一两行代码轻松实现它。
所以,我决定写我自己的代码,并与您分享!
我的实现对您来说有多容易?
嗯,我假设您已经在应用程序中实现了表单身份验证,并在 `web.config` 中配置了相关内容。所以,现在要实现基于角色的授权,您只需要做以下三件简单的事情,总共需要最多五分钟时间来实现。
- 将 `RoleBasedFormAuthentication.dll`(您可以从本文下载,包含源代码)添加到您的网站/项目中。
- 在对用户进行身份验证后,不要调用以下方法
FormsAuthentication.RedirectFromLoginPage(userName,createPersistantCookie);
而是调用以下方法
FormsAuthenticationUtil.RedirectFromLoginPage(userName, commaSeperatedRoles, createPersistantCookie);
- 在 `Global.asax` 文件中添加以下代码,或者,如果代码已经存在,则进行更改
protected void Application_AuthenticateRequest(Object sender,EventArgs e) { FormsAuthenticationUtil.AttachRolesToUser(); }
这样,您就完成了。
好奇吗?细节如下
我创建了我自己的身份验证/授权代码版本,并解决了上述三个问题,如下所示
解决“超时”问题
在创建 `FormsAuthenticationTicket` 对象时,我们需要提供五个参数。看一下创建身份验证票证的以下方法
/// <summary>
/// Creates and returns the Forms authentication ticket
/// </summary>
/// <param name="userName">User name</param>
/// <param name="commaSeperatedRoles">Comma separated roles for the users</param>
/// <param name="createPersistentCookie">True or false
/// whether to create persistant cookie</param>
/// <param name="strCookiePath">Path for which the authentication ticket is valid</param>
private static FormsAuthenticationTicket CreateAuthenticationTicket(string userName,
string commaSeperatedRoles, bool createPersistentCookie, string strCookiePath)
{
string cookiePath = strCookiePath == null ?
FormsAuthentication.FormsCookiePath : strCookiePath;
//Determine the cookie timeout value from web.config if specified
int expirationMinutes = GetCookieTimeoutValue();
//Create the authentication ticket
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1, //A dummy ticket version
userName, //User name for whom the ticket is issued
DateTime.Now, //Current date and time
DateTime.Now.AddMinutes(expirationMinutes), //Expiration date and time
createPersistentCookie, //Whether to persist cookie on client side. If true,
//The authentication ticket will be issued for new sessions from the same client
//PC
commaSeperatedRoles, //Comma separated user roles
cookiePath); //Path cookie valid for
return ticket;
}
请注意第三个参数 `DateTime.Now.AddMinutes(expirationMinutes)`。在这里,我们期望 `expirationMinutes` 变量的值从 `
所以,我不得不实现并使用以下方法来从 `web.config`(如果指定了)读取 `timeout` 属性,并在创建 `FormsAuthenticationTicket` 对象时设置该值。
/// <summary>
/// Retrieves cookie timeout value in the <forms></forms>
/// section in the web.config file as this
/// value is not accessible via the FormsAuthentication or any other built in class
/// </summary>
/// <returns></returns>
private static int GetCookieTimeoutValue()
{
int timeout = 30; //Default timeout is 30 minutes
XmlDocument webConfig = new XmlDocument();
webConfig.Load(HttpContext.Current.Server.MapPath("web.config"));
XmlNode node = webConfig.SelectSingleNode("/configuration/" +
"system.web/authentication/forms");
if (node != null && node.Attributes["timeout"] != null)
{
timeout = int.Parse(node.Attributes["timeout"].Value);
}
return timeout;
}
完成此操作后,系统便能够正确地从 `web.config` 读取“timeout
”值并将其设置在身份验证票证对象中。
解决“无 cookie”问题
如果 `web.config` 中的“cookieless
”属性设置为“UseUri
”,或者由于任何原因浏览器不支持 cookie,或者,浏览器支持 cookie 但在设置中禁用了它,那么表单身份验证会将身份验证票证写入 URL,并在后续请求中读回票证。
因此,当我们需要自己创建身份验证票证以实现基于角色的授权时,我们也需要实现相同的逻辑,否则就会出现问题。因此,我们需要根据上述情况确定是应该将票证嵌入 Cookie 中,还是应该将票证写入 URL。以下代码可以实现此目的
/// <summary>
/// Creates Forms authentication ticket and writes it in URL or embeds it within Cookie
/// </summary>
/// <param name="userName">User name</param>
/// <param name="commaSeperatedRoles">Comma separated roles for the users</param>
/// <param name="createPersistentCookie">True or false whether
/// to create persistant cookie</param>
/// <param name="strCookiePath">Path for which
/// the authentication ticket is valid</param>
private static void SetAuthCookieMain(string userName, string commaSeperatedRoles,
bool createPersistentCookie, string strCookiePath)
{
FormsAuthenticationTicket ticket =
CreateAuthenticationTicket(userName, commaSeperatedRoles,
createPersistentCookie, strCookiePath);
//Encrypt the authentication ticket
string encrypetedTicket = FormsAuthentication.Encrypt(ticket);
if (!FormsAuthentication.CookiesSupported)
{
//If the authentication ticket is specified not to use cookie, set it in the URL
FormsAuthentication.SetAuthCookie(encrypetedTicket, false);
}
else
{
//If the authentication ticket is specified to use a cookie,
//wrap it within a cookie.
//The default cookie name is .ASPXAUTH if not specified
//in the <forms> element in web.config
HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName,
encrypetedTicket);
//Set the cookie's expiration time to the tickets expiration time
authCookie.Expires = ticket.Expiration;
//Set the cookie in the Response
HttpContext.Current.Response.Cookies.Add(authCookie);
}
}
以下代码块是这里的关键
if (!FormsAuthentication.CookiesSupported)
{
//If the authentication ticket is specified not to use cookie, set it in the URL
FormsAuthentication.SetAuthCookie(encrypetedTicket, false);
}
`FormsAuthentication.SetAuthCookie()` 方法可能会引起误解。顾名思义,它似乎用身份验证票证创建了表单身份验证 cookie。是的,它确实如此。但是,如果浏览器不支持 cookie,它会将加密的授权票证内容设置到 URL 中。因此,现在,如果浏览器不支持 cookie,表单身份验证和基于角色的授权将对我们正常工作。
请注意,在进行上述代码更改后,我们还需要修改在后续请求中设置用户角色的代码(在 `Global.asax` 的 `Application_AuthenticateRequest()` 事件中)。
/// <summary>
/// Adds roles to the current User in HttpContext
/// after forms authentication authenticates the user
/// so that, the authorization mechanism can authorize
/// user based on the groups/roles of the user
/// </summary>
…
if (HttpContext.Current.User != null)
{
if (HttpContext.Current.User.Identity.IsAuthenticated)
{
if (HttpContext.Current.User.Identity is FormsIdentity)
{
FormsIdentity id = (FormsIdentity)HttpContext.Current.User.Identity;
FormsAuthenticationTicket ticket = (id.Ticket);
if (!FormsAuthentication.CookiesSupported)
{
//If cookie is not supported for forms authentication, then the
//authentication ticket is stored in the URL, which is encrypted.
//So, decrypt it
ticket = FormsAuthentication.Decrypt(id.Ticket.Name);
}
// Get the stored user-data, in this case, user roles
if (!string.IsNullOrEmpty(ticket.UserData))
{
string userData = ticket.UserData;
string[] roles = userData.Split(',');
//Roles were put in the UserData property in the authentication ticket
//while creating it
HttpContext.Current.User =
new System.Security.Principal.GenericPrincipal(id, roles);
}
}
}
}
我只是在 `FormsAuthenticationTicket ticket = (id.Ticket);
` 之后添加了以下代码。
if (!FormsAuthentication.CookiesSupported)
{
//If cookie is not supported for forms authentication, then the
//authentication ticket is stored in the URL, which is encrypted.
//So, decrypt it
ticket = FormsAuthentication.Decrypt(id.Ticket.Name);
}
这就是“无 cookie”问题的解决方案。
将代码解耦到可重用的 DLL 中
“封装”的黄金法则告诉我们,您应该将复杂性封装起来,不暴露给外部世界。那么,为什么不将所有这些混乱的代码封装到一个盒子里呢?为什么不保持代码的整洁呢?
受到这一原则的启发,我创建了一个类库(“RoleBasedFormAuthentication
”),并将所有与身份验证和授权相关的代码移动到其中。我在类库中创建了一个 `FormsAuthenticationUtil` 类,并在其中实现了以下核心可重用 `private` 方法
私有方法
/// <summary>
/// Creates and returns the Forms authentication ticket
/// </summary>
private static FormsAuthenticationTicket CreateAuthenticationTicket(…)
/// <summary>
/// Creates a Forms authentication ticket using the private
/// method CreateAuthenticationTicket() and writes
/// it in URL or embeds it within Cookie
/// </summary>
private static void SetAuthCookieMain(…)
/// <summary>
/// Creates a Forms authentication ticket and sets it within URL or Cookie
/// using the SetAuthCookieMain() private method, and redirects
/// to the originally requested page
/// </summary>
private static void RedirectFromLoginPageMain(…)
以上三个是供外部使用的 `public` 方法所调用的核心方法。以下是类中实现的 `public` 方法(及其重载版本)
公共方法
/// <summary>
/// Creates Forms authentication ticket and redirects
/// to the originally requested page. Uses the
/// RedirectFromLoginPageMain() private method
/// </summary>
public static void RedirectFromLoginPage(…)
/// <summary>
/// Creates a Forms authentication ticket and writes it
/// in URL or embeds it within Cookie. Uses the
/// SetAuthCookieMain() private method
/// </summary>
public static void SetAuthCookie(…)
/// <summary>
/// Adds roles to the current User in HttpContext
/// after forms authentication authenticates the user
/// so that, the authorization mechanism can authorize
/// user based on the groups/roles of the user
/// </summary>
public static void AttachRolesToUser()
这些 `public` 方法由客户端 Web 应用程序调用,以实现表单身份验证和基于角色的授权。将所有与身份验证和授权相关的逻辑解耦并实现到类库中,使我们能够实现 ASP.NET 应用程序中的基于角色的授权
- 在很短的时间内。
- 以正确的方式。
- 以更简洁、更智能的方式。
示例项目
下载示例 ASP.NET 网站应用程序(使用 Visual Studio 2008,Framework 3.5 创建),将其解压缩(FormsAuthorization.zip)到方便的位置。使用 Visual Studio 打开网站,或者创建指向示例网站 Web 根文件夹的 IIS 网站/虚拟目录。假设您已创建 IIS 网站/虚拟目录,请执行以下操作来验证身份验证和基于角色的授权以及提到的问题。
测试授权
- 在浏览器中访问以下 URL:https:///FormsAuthorization/Admin/Default.aspx。系统会将您重定向到登录页面。提供“Administrator/123”作为登录凭据并按“Login”。您将进入一个显示“Hello Admin”消息的页面。
通过注销,或者打开一个新的浏览器窗口/标签页,再次访问相同的 URL。但这次,请提供“John/123”作为凭据。系统不会让您访问该页面,而是会保留登录屏幕不变。
查看网站的 `web.config` 文件,您会发现只有“Admin”角色被允许访问此 URL,而所有其他用户都被拒绝访问。这就是为什么 John 的凭据(他是“User”角色的成员)无法访问仅属于“Admin”角色的 URL。
<location path="Admin"> <system.web> <authorization> <allow roles="Admin"/> <deny users="*"/> </authorization> </system.web> </location>
- 在浏览器中访问以下 URL:https:///FormsAuthorization/User/Default.aspx。系统会将您重定向到登录页面。提供“John/123”作为登录凭据并按“Login”。您将进入一个显示“Hello John”消息的页面。
通过注销,或者打开一个新的浏览器窗口/标签页,再次访问相同的 URL。但这次,请提供“Administrator /123”作为凭据。系统不会让您访问该页面,而是会保留登录屏幕不变。
查看网站的 `web.config` 文件,您会发现只有“User”角色被允许访问此 URL,而所有其他用户都被拒绝访问。这就是为什么 Admin 的凭据(他是“Admin”角色的成员)无法访问仅属于“User”角色的 URL。
<location path="User"> <system.web> <authorization> <allow roles="User"/> <deny users="*"/> </authorization> </system.web> </location>
- 在浏览器中访问以下 URL:https:///FormsAuthorization/Public/Default.aspx。系统将显示“Hello, this is a public page”。正如您所理解的,这是一个公共页面,无需任何凭据即可访问。
查看网站的 `web.config` 文件,您会发现所有用户都被允许访问此 URL。因此,无需登录凭据即可访问。
<location path="Public"> <system.web> <authorization> <allow users="*"/> </authorization> </system.web> </location>
测试“timeout”属性
- 在 `web.config` 文件中,将“
timeout
”属性值更改为“1”。<forms name="login" timeout="1" loginUrl="Login.aspx"></forms>
- 在浏览器中访问以下 URL:https:///FormsAuthorization/Admin/Default.aspx,并使用“Administrator/123”作为登录凭据进行登录。
- 在接下来的 1 分钟或更长时间内不要做任何操作,当一分钟过去后,刷新页面。由于身份验证 cookie 在此期间已过期,系统会将您重定向到登录页面。这表明系统能够正确地从 `web.config` 读取并应用“
timeout
”属性。
测试“cookieless”属性
- 在 `web.config` 中,将“
cookieless
”属性值更改为“UseUri
”。<forms name="login" timeout="120" loginUrl="Login.aspx" cookieless="UseUri"></forms>
- 在浏览器中访问以下 URL:https:///FormsAuthorization/Admin/Default.aspx,并使用“Administrator/123”作为登录凭据进行登录。系统将成功登录您。
- 查看地址栏中的 URL。它应该类似于以下内容
https:///FormsAuthorization/(F(Oz5JC7onSkVsmb6....))/Admin/Default.aspx
您可以看到身份验证票证已被加密并包含在 URL 中(实际 URL 应该很长,为节省空间,URL 中加密票证的其余部分已用一些点省略)。这表明系统能够将身份验证票证写入 URL 并正确地执行身份验证和授权。
示例网站“FormsAuthorization”使用类库“RoleBasedFormsAuthentication.dll”来实现身份验证和基于角色的授权。类库的源代码也可以在本文中下载(RoleBasedFormsAuthentication.zip)。
结论
尽管 Membership 是 ASP.NET 中一项非常出色的功能,但基本的表单身份验证不会被淘汰,并且会不断被使用。本文没有讨论任何“高深技术”,我只希望我的努力能帮助您实现一个健壮的表单身份验证/授权系统,并为您节省宝贵的时间。我希望在 ASP.NET 框架的未来版本中,`FormsAuthentication.RedirectFromLoginPage()` 和其他相关方法中能够包含 `commaSeperatedRoles` 参数(或类似参数)。
编程愉快!
历史
- 2009年5月30日:初版
- 2009年12月3日:更新下载文件