65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (118投票s)

2009年5月30日

CPOL

11分钟阅读

viewsIcon

395727

downloadIcon

12605

本文介绍了一种在 ASP.NET 中实现基于角色的授权与表单身份验证的正确且更智能的方法。

问题空间

令人遗憾但事实是,“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 应用程序中尝试实现它们时发现了这些问题。我按照其中一篇文章的建议操作,发现授权工作正常。但是,为了满足客户的要求,我不得不增加 `` 元素中的 cookie `timeout` 属性,并将其设置为“120”(120 分钟),结果发现,超时值更改对应用程序没有影响。深入研究后,我惊讶地发现系统从未读取增加的值;相反,它总是读取默认值“30”。

我很好奇地调查了这个问题,并发现了另一个问题。我设置了 `` 元素中的 `cookieless="UseUri"`,以测试当客户端浏览器禁用 cookie 时,表单身份验证是否还能通过(通过在请求 URL 中写入身份验证票证)发挥作用。再次令人惊讶的是,现在系统停止对用户进行身份验证了!

此外,我快速查看了(为实现基于角色的授权而编写的)身份验证/授权代码,并想,为什么我需要编写所有这些代码?任何人都可以通过更改一两行代码轻松实现它。

所以,我决定写我自己的代码,并与您分享!

我的实现对您来说有多容易?

嗯,我假设您已经在应用程序中实现了表单身份验证,并在 `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` 变量的值从 `` 部分的 `timeout` 属性中读取。但是,不幸的是,就像 `FormsAuthentication.FormsCookiePath` 属性(它读取 `` 部分中指定的路径配置值)一样,`FormsAuthentication` 或任何其他类都没有提供读取 `timeout` 属性值的方法。我不知道为什么。

所以,我不得不实现并使用以下方法来从 `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日:更新下载文件
表单身份验证和基于角色的授权:一种更快、更简单、更正确的方法 - CodeProject - 代码之家
© . All rights reserved.