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

基于声明的身份验证和授权

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (18投票s)

2013年9月1日

CPOL

16分钟阅读

viewsIcon

138624

使用 MembershipReboot 和 Thinktecture.IdentityModel.45 进行身份验证和授权

引言

您可以在此位置下载本文的 Visual Studio 解决方案。包含所有 Nuget 二进制文件,大小约为 57 MB(无法在此 CodeProject.com 上托管)。

ASP.NET 中开箱即用的身份验证和授权机制已经过时且陈旧。我从来都不太喜欢 membership provider。幸运的是,微软已经开发了一种替代方法来实现身份验证和授权,即基于声明的安全,它现在是 System.IdentityModel 命名空间的一部分。还有一些非常有才华的开发者开发了一些开源项目,使得使用支持声明的身份验证和授权变得容易。

在本文中,我计划向您展示一个 ASP.NET MVC 4 应用程序的简单、基础的安全实现。此实现将使用

  1. MembershipReboot 库进行身份验证;以及
  2. Thinktecture.IdentityModel.45 进行授权。

我打算分阶段实现此示例应用程序,以便将每个阶段(或快照)的代码包含在本文附带的下载代码中。这样做的目的是让读者跟随本文中的代码和下载代码,并观察其演变过程。每一步都旨在演示身份验证/授权的工作原理。我的重点将放在安全配置和代码上。您将能够通过其中一个快照来了解情况,而不是试图从最终产品中弄清楚所有内容。

请注意,我不会关注外观和感觉。我将保持低保真度,以免分散本文的重点。

更新:在撰写本文之后,我了解到微软为 ASP.NET 推出了一个新的成员资格系统,名为 ASP.NET Identity。该系统似乎不依赖于旧的 Membership API。此外,它支持声明。鉴于此,对于您的 ASP.NET 应用程序,它可能是一个值得考虑的身份验证系统。

身份验证 – 准备工作

在示例应用程序中,我将使用 Forms 身份验证。请注意,这并不意味着我必须使用 Membership provider。Membership API 是可用的,但肯定不是强制性的。它只是一个数据存储的抽象。我选择使用 Brock Allen 的库,MembershipReboot,尽管它的名字如此,但它与 Membership provider 无关。事实上,它根本不是一个 provider。它是一个执行大量与身份验证相关的功能的库,包括

  1. 创建新用户,包括密码的加密/哈希处理
  2. 验证用户身份
  3. 启用用户解锁
  4. 重置用户密码

开始使用 MembershipReboot

开始之前,请使用 **Visual Studio 2012** 中的 **ASP.NET MVC 4** Web 应用程序项目模板创建一个新项目。选择 **basic** 模板。由于 MembershipReboot 有一个 Nuget 包,请使用 Nuget 下载 **MembershipReboot** DLL 并将其引用到您的项目中。接下来您需要的是数据存储。我选择了一个 SQL Server 2008 R2 (v.661) 数据库。您可以随意命名此数据库,并且最棒的是,我们即将创建的表将只是您大型数据库(例如,用于存储定制应用程序数据的数据库)的一部分普通表。

接下来是那些表。创建它们的最简单方法是运行 SQL 脚本,这些脚本可以与 MembershipReboot 项目的源代码一起下载。您可以 从这里 下载源代码,您会在 Migrations.SqlServer\Sql 文件夹中找到四个 SQL 脚本。为了方便您,我已将这些脚本包含在本文的下载代码中。您应该按照以下顺序运行这些脚本:

  1. 201301101956394_InitialMigration.sql
  2. 201302251410424_ExpandKeyAndAddAccountCloseFields.sql
  3. 201304152032598_NameId.sql
  4. 201304160322055_VerificationPurpose.sql

现在我们有了一个数据库(我叫它 MembershipReboot,这是因为我缺乏想象力),其中包含两个表:

  1. UserAccounts
  2. UserClaims

这是我们所有身份验证相关信息的存储。也就是说,创建用户、删除用户、重置密码、锁定用户、解锁用户、登录用户、登出用户等。

快速跳到创建用户

此时,我们需要向数据库添加一个用户。我们不能仅通过后端简单的 SQL 语句来添加。用户的密码需要正确哈希。鉴于此,我们需要使用某种前端来创建用户,该前端将执行正确的哈希处理。下载代码中的应用程序包含一个专门用于此目的的页面。

因此,我们将绕道而行。要添加用户:

  1. 在下载的代码中,打开名为 ClaimsAspMvc 的目录中的 Visual Studio 解决方案。
  2. 运行解决方案,点击页面上唯一的超链接导航到 **创建新用户** 页面。
  3. 填写表单以添加用户。
  4. 使用 SQL Server Management Studio 检查新用户是否已存在于数据库中。

添加了新用户后,我们就可以继续了。但首先,关闭该解决方案,因为这是最终目标。

为 MembershipReboot 配置 Web.config

到目前为止,我们已经创建了一个 SQL Server 数据库(我使用的是 SQL Server 2008 R2),并创建了一个 ASP.NET MVC 4 项目,并通过 Nuget 将 MembershipReboot 库添加到解决方案中。现在我们需要配置应用程序以使用 Forms Authentication 进行身份验证,并配置 MembershipReboot

1. 从 Web.config 文件中删除以下元素

<profile defaultProvider="DefaultProfileProvider">
  <providers>
    <add name="DefaultProfileProvider" 
      type="System.Web.Providers.DefaultProfileProvider, System.Web.Providers, 
            Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" 
      connectionStringName="DefaultConnection" applicationName="/" />
  </providers>
</profile>
<membership defaultProvider="DefaultMembershipProvider">
  <providers>
    <add name="DefaultMembershipProvider" 
      type="System.Web.Providers.DefaultMembershipProvider, System.Web.Providers, 
            Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" 
      connectionStringName="DefaultConnection" enablePasswordRetrieval="false" 
      enablePasswordReset="true" requiresQuestionAndAnswer="false" 
      requiresUniqueEmail="false" maxInvalidPasswordAttempts="5" 
      minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0" 
      passwordAttemptWindow="10" applicationName="/" />
  </providers>
</membership>
<roleManager defaultProvider="DefaultRoleProvider">
  <providers>
    <add name="DefaultRoleProvider" 
      type="System.Web.Providers.DefaultRoleProvider, System.Web.Providers, 
            Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" 
      connectionStringName="DefaultConnection" applicationName="/" />
  </providers>
</roleManager>

由于我的示例应用程序根本不使用任何提供程序,因此我不希望它们在我的 config 文件中造成混乱。

2. 将以下节添加到 Configsections 元素内的 Web.config 文件中

(注意,如果其中任何一个已经存在,则无需添加两次。)

<section name="system.identityModel" 
  type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, 
        Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
<section name="system.identityModel.services" 
   type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, 
        System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
<section name="membershipReboot" 
   type="BrockAllen.MembershipReboot.SecuritySettings, BrockAllen.MembershipReboot"/>
<section name="entityFramework" 
   type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, 
         Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" 
   requirePermission="false" />

通过添加这些节,我们使配置元素可在此 Web.config 文件中使用。注意具有 name 属性 membershipReboot 的节。该配置节来自 MembershipReboot 项目(该元素的 type 属性对此有所体现)。

3. 添加数据库的 ConnectionString

这是用于连接到您上面创建的数据库,以存储身份验证信息。

<connectionStrings>
    <add name="MembershipReboot" 
      connectionString="server=localhost;database=MembershipReboot;trusted_connection=yes;" 
      providerName="System.Data.SqlClient" />
</connectionStrings> 

4. 确保 Web.config 文件中存在以下节,作为 <system.web> 元素的子元素

<authentication mode="Forms">
      <forms loginUrl="~/Admin/Login" timeout="2880" />
</authentication> 

该元素将应用程序配置为使用Forms Authentication,并在用户尚未进行身份验证时将重定向到 /Admin/Login 路由。请注意,我添加了一个名为 Admin 的控制器和一个名为 Index 的视图。我在创建此解决方案时特意选择了 **basic** 模板,因为我不想在创建解决方案时已经存在任何现成的身份验证代码。

这是下载代码中第 1 阶段代码的状态。如果您现在确保 ConnectionString 指向您的数据库实例,并运行应用程序(使用第 1 阶段代码),您会发现一个要求您输入消息的页面。在文本框中输入消息并单击按钮。您应该立即被重定向到登录页面。现在我们有了应用程序身份验证代码的最基本起点。

快速查看 AdminControllerLogin Action 方法。我几乎是从 MembershipReboot 源代码附带的示例应用程序中逐字复制的。请注意,我没有使用 IOC 容器来处理 UserAuthenticationServiceClaimsBasedAuthenticationService 的依赖注入,因为我不想分散本文的关键目的。在实际应用中,这些服务会通过 IOC 容器注入。我将在示例应用程序的最终版本中清理这些并添加正确的依赖注入。

现在我们需要对 MembershipReboot 进行一些配置,并设置 Session Authentication Module (SAM),以便身份验证信息可以在请求之间持久化。

完成身份验证部分

现在我们需要添加 MembershipReboot 节。请注意,这不是 provider,不应与传统的 ASP.NET providers 混淆。这是一个自定义配置节,是 MembershipReboot 库的一部分。需要将其添加为配置元素的子元素。

<membershipReboot
    connectionStringName="MembershipReboot" 
    requireAccountVerification="true"
    emailIsUsername="false"
    multiTenant="false"
    passwordHashingIterationCount="0"
    accountLockoutDuration="00:01:00"
    passwordResetFrequency="0"
/> 

它是配置 MembershipReboot 行为的一种方式。其他选项包括:

  1. allowAccountDeletion
  2. emailIsUsername
  3. requireAccountVerification
  4. allowLoginAfterAccountCreation

显然,connectionStringName 需要设置,以便 MembershipReboot 库可以访问我们之前创建的数据库表。该库使用 EntityFramework 5 作为其 ORM。但是,由于是开源的,您可以完全重写 MembershipReboot 库的数据访问代码。

接下来我们需要添加到 Web.config 中的是 SAM 元素,位于 system.WebServer 元素下。

<modules>
      <add name="SessionAuthenticationModule" 
        type="System.IdentityModel.Services.SessionAuthenticationModule, 
              System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, 
              PublicKeyToken=b77a5c561934e089" />      
</modules> 

SessionAuthenticationModule 使我们能够在 http 请求之间持久化身份验证凭据。(由于这会覆盖 IIS 设置,因此您至少需要使用 IIS Express 配合您的项目。Cassini 不行。示例代码使用的是 IIS Express。)

我需要添加到 Web.config 的最后一项是以下节:

<system.identityModel.services>
    <federationConfiguration>
      <cookieHandler requireSsl="false" />
    </federationConfiguration>
</system.identityModel.services>

理想情况下,如果这是一个面向公众的应用程序,我们将使用证书对用于身份验证的 cookie 进行加密(就像 Google 对其搜索页面所做的那样——它会加密搜索)。但是,我不会在这里费劲去创建一个自签名证书,而是为了本示例应用程序的目的,接受不使用 SSL 和未受保护的 cookie。

我现在将通过一些 Fiddler 的截图来演示 SAM 如何持久化这些凭据。

图 1 – 显示了我在输入一些文本并单击“提交消息”按钮后发出的请求。由于我尚未进行身份验证,因此会像 Forms 身份验证一样重定向到登录页面。您可以在左侧的跟踪窗口中看到请求 10 的 302 重定向。

图 2 – 我选择了跟踪窗口中编号为 15 的请求。您可以在响应窗口中看到,当我使用有效的密码和用户名成功进行身份验证并单击“登录”按钮后,SAM 发出的 cookie 值。

图 3 – 登录后,在后续请求中,您可以看到发送的 cookie。这些 cookie 还包含已登录用户的所有声明。稍后将对此进行更详细的介绍。

如果您现在打开下载代码的第 2 阶段,我们已经使用 MembershipReboot 库实现了基本身份验证。用法如下:

  1. 首次加载应用程序时,您将作为匿名用户访问它。然后,在文本框中输入一条消息并单击提交按钮。
  2. 这将把您重定向到登录页面,您可以在其中输入您的登录详细信息。输入您的用户的用户名和密码,您将被带回到原始屏幕。
  3. 您可以单击“注销”链接进行注销。然后再次登录,以确认一切正常。

注意:有一些很棒的浏览器扩展程序可以清除 cookie。我为 Chrome 使用 Click&Clean。在本文中,当您需要清除浏览器 cookie 来尝试代码时,这些扩展程序会很有用。

授权 - 准备工作

现在我们转向授权。也就是说,安全措施规定了在给定主体所拥有的声明、正在执行的操作和涉及的资源的组合下,是否可以执行某个操作。其思想是评估是否允许具有特定声明的主体对特定资源执行操作。可以用一个方程来表述:

Evaluate if a principal with claims x is permitted to do action y on resource z  

(摘自 Dominick Baier 在此博客文章中的评论)。

开始使用 Thinktecture.IdentityModel

Thinktecture.IdentityModel 是一个出色的库,它提供了大量辅助方法,使得在支持声明的环境中进行授权相当直接(尽管我花了一段时间才把所有部分都组合起来)。

首先,我们需要使用 Nuget 下载 Thinktecture.IdentityModel。您会注意到,为了在之前的阶段中从 HomeController 重定向到 AdminController,我在 HomeController 的第二个 Index 方法上加了一个 Authorize 属性。该属性仅支持旧的实现方式。如果您想集成支持声明的授权,则需要继承该类。而 Thinktecture.IdentityModel 中的 ClaimsAuthorize 属性正是这样做的。

在第 3 阶段的代码中,您会看到我这样装饰了该操作方法:

[ClaimsAuthorize("Write", "ImportantMessage")]
[HttpPost]
public ActionResult Index(string message)
{
    return View();
} 

接下来的部分非常重要,它演示了如何将授权逻辑外部化。我们需要编写一个继承自 ClaimsAuthorizationManager(位于 System.Security.Claims 命名空间)的类。有一个名为 CheckAccess 的方法需要我们重写。在这个方法中,就授权而言,世界是您的。这就是这种授权方法的优势。您可以自由执行任何类型的检查。您可以访问数据存储以获取决定场景权限的数据。

我创建的用于执行此授权的类如下:

public class AreWeAllowedToDoItManager : ClaimsAuthorizationManager
{
  public override bool CheckAccess(AuthorizationContext context)
  {
    var resource = context.Resource.First().Value;
    var claims = context.Principal.Claims.ToList();
 
    switch (resource)
    {
      case "ImportantMessage":
      {
        if (PrincipalCanPerformActionOnResource(context))
          return true;
        break;
      }
      default:
      {
        throw new NotSupportedException(string.Format("{0} is not a valid resource", resource));
      }
    }
 
    return false;
  }
 
  private bool PrincipalCanPerformActionOnResource(AuthorizationContext context)
  {
    var action = context.Action.First().Value;
 
    switch (action)
    {
      case "Write":
      {
        if (context.Principal.HasClaim("http://dave.com/ws/identity/claims/messenger", 
                                       "ImportantMessenger"))
        {
          return true;
        }
        break;
      }
      default:
      {
        throw new NotSupportedException(string.Format("{0} is not a valid action", action));
      }
    }
 
    return false;
  }
}

我像这样在 Web.config 文件中将其关联起来(作为 Configuration 元素的子元素):

<system.identityModel>
  <identityConfiguration>  
    <claimsAuthorizationManager type="ClaimsAspMvc.AreWeAllowedToDoItManager, ClaimsAspMvc" />
  </identityConfiguration>
</system.identityModel>

在项目中(第 3 阶段的项目),您会看到我在一个名为 CustomClaimTypes 的类中创建了一个 Messenger 常量,其值为:“http://dave.com/ws/identity/claims/messenger”。由于我是在自定义声明,所以可以自定义 URI!ClaimTypes 常量最终都只是 URI string。微软为 ClaimTypes 创建的 URI 可以在这里查看

现在,您可以将此声明添加到 UserClaims 表中。使用 db 后端,只需在 UserAccountID 列中添加用户的 ID,在 Type 列中添加该常量的 URI,并在 Value 列中添加“ImportantMessenger”。

UserAccountID 类型
3 http://dave.com/ws/identity/claims/messenger ImportantMessenger

AreWeAllowedToDoItManagerCheckAccess 方法中,我们知道正在验证的事实是:

Evaluate if the current user with a Messenger claim of ImportantMessenger 
         is permitted to do a Write action on the ImportantMessage resource

因此,打开下载代码的第 3 阶段,确保您使用的用户已注销(注销他们或清除浏览器 cookie)。然后,按照以下步骤操作:

  1. 输入消息并单击“提交消息”按钮,然后观察它如何将您转移到登录页面。由于匿名用户没有 CustomClaimTypes.Messenger 声明,因此它无法访问 HomeController 上的 Index 操作(HttpPost 版本)。
  2. 登录。您将被重定向回 Index 页面,并显示 302 请求。
  3. 在输入框中键入一条消息并单击“提交消息”按钮。您应该会看到消息出现在输入框下方。

成功!已登录的用户已通过授权检查。

您可以尝试通过尝试“提交消息”来测试这一点,但使用已登录但没有 CustomClaimTypes.Messenger 声明的用户。继续进行此操作,并验证用户是否始终被重定向回登录页面,表明未通过 ClaimsAuthorize 检查。

稍后,我将在项目中添加一个页面,您可以在其中从前端为用户添加声明。我只触及了这个库的表面,并且非常有兴趣探索其丰富的功能。

几点最后的评论/观察

基于角色的检查 – 如果您实在需要

如果您出于某种原因需要使用基于角色的授权,新的 System.IdentityModel API 支持使用实现 IPrincipal 接口的对象来执行此操作。为了演示,您可以在 Index.cshtml 中添加以下代码:

if(User.Identity.IsAuthenticated)
{
  <text>Hello </text>@User.Identity.Name<text>, 
    you are logged in!</text> @Html.ActionLink("Logout", "LogOut", "Home")

  System.Security.Claims.ClaimsPrincipal cp = new ClaimsPrincipal(User.Identity);

  if (cp.IsInRole("Maintainer"))
  {
    <div>If you can see this, you are logged in and in the Maintainer role.</div>
  }
}  

角色 API 将应用于该表中具有 URI(这是 ClaimTypes.Role 常量解析到的内容)的任何声明,并且该声明的值为“Maintainers”。

然后,您可以通过向 UserClaims 表添加一行来将用户添加到该角色:

UserAccountID 类型
3 http://schemas.microsoft.com/ws/2008/06/identity/claims/role Maintainers

这里最重要的收获是,Roles API 仍然可以与新的 ClaimsPrincipal 类一起使用,并且仍然可以使用 IsInRole 等方法。

AntiForgeryTokens 和 CreateUser 屏幕

如果您向 CreateUser 屏幕添加 AntiForgeryToken (您应该这样做),您会发现将抛出以下 InvalidOperationException

A claim of type 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier' or 
'http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider' was not present 
on the provided ClaimsIdentity. To enable anti-forgery token support with claims-based authentication, 
please verify that the configured claims provider is providing both of these claims 
on the ClaimsIdentity instances it generates. If the configured claims provider instead 
uses a different claim type as a unique identifier, it can be configured by setting 
the static property AntiForgeryConfig.UniqueClaimTypeIdentifier.

有两种方法可以成功验证 AntiForgeryTokens 而不抛出 AntiForgeryTokens 相关的 InvalidOperationException。这两种方法在错误消息本身中都得到了非常清晰的解释:

  1. 第一种方法是向主体 Claims 集合添加以下任一声明:
    1. http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
    2. http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider
    这是更难走的路,并且有一个非常令人信服的理由不选择下面的第二种方法。
  2. 第二种方法是在 MvcApplication 类(在 Global.asax.cs 中)的 Application_Start 方法中添加以下代码行:
    AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Name;
    (因为这是我们用作 User 唯一标识符的 ClaimType。如果您使用的是电子邮件地址,则应分配 ClaimTypes.Email)。

Cookie 大小 – 是否需要每个声明?

回顾 **图 2**,我们可以看到 payload 相当大,因为它承载了已登录用户的所有 Claims 信息。在 Claims 之前,cookie 只包含用户名和密码。如果您不需要 cookie 中的所有声明信息,有一种方法可以关闭该功能(可以说)。

MvcApplication 类中,您需要重写 Init() 方法并添加以下代码:

public override void Init()
{
    var sam = FederatedAuthentication.SessionAuthenticationModule;
    sam.IsReferenceMode = true;
}

这启用了会话令牌的服务器端缓存,Brock Allen 在这篇博文中对此进行了说明。这导致 cookie 大小从 2026 字节减少到 558 字节。

配置 SMTP 以发送电子邮件

身份验证/授权方案的一部分是能够重置密码、确认新用户创建以及执行其他类似任务。在最终代码中,我使用了 MembershipReboot 库中的 NotificationService 类。这使我可以通过 Web.config 轻松配置 SMTP 服务器,如下所示:

<system.net>
    <mailsettings>
      <smtp from="somesecureguy@gmail.com">
        <network host="smtp.gmail.com" username="somesecureguy@gmail.com" 
                password="1C8(3*8b%$Et7y" port="587" enablessl="true">
      </network></smtp>
    </mailsettings>
</system.net>

这些详细信息应该可以直接为您工作,或者至少直到 Google 取消该帐户(我只为本文设置了该帐户)。

最终想法

我只介绍了这两个库以及这种声明机制的皮毛。显然,由于这些库是开源的,因此可以非常轻松地根据您的需求定制这些内容。我计划将此示例的最终代码演变成一个更有趣的示例。但目前,它提供了一个令人满意的载体来展示这些库的基础知识。

希望您能像我一样喜欢和它们一起玩!

历史

文章

版本 日期 摘要
1.0 2013 年 9 月 1 日 原始发布文章
1.1 2013 年 9 月 13 日 添加了关于 ASP.NET Identity 的更新
1.2 2014 年 9 月 13 日 修正了 system.identityModel.services 元素的错误大小写

代码

版本 日期
1.0 2013 年 9 月 1 日
© . All rights reserved.