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






4.96/5 (18投票s)
使用 MembershipReboot 和 Thinktecture.IdentityModel.45 进行身份验证和授权
引言
您可以在此位置下载本文的 Visual Studio 解决方案。包含所有 Nuget 二进制文件,大小约为 57 MB(无法在此 CodeProject.com 上托管)。
ASP.NET 中开箱即用的身份验证和授权机制已经过时且陈旧。我从来都不太喜欢 membership provider。幸运的是,微软已经开发了一种替代方法来实现身份验证和授权,即基于声明的安全,它现在是 System.IdentityModel
命名空间的一部分。还有一些非常有才华的开发者开发了一些开源项目,使得使用支持声明的身份验证和授权变得容易。
在本文中,我计划向您展示一个 ASP.NET MVC 4 应用程序的简单、基础的安全实现。此实现将使用
- MembershipReboot 库进行身份验证;以及
- Thinktecture.IdentityModel.45 进行授权。
我打算分阶段实现此示例应用程序,以便将每个阶段(或快照)的代码包含在本文附带的下载代码中。这样做的目的是让读者跟随本文中的代码和下载代码,并观察其演变过程。每一步都旨在演示身份验证/授权的工作原理。我的重点将放在安全配置和代码上。您将能够通过其中一个快照来了解情况,而不是试图从最终产品中弄清楚所有内容。
请注意,我不会关注外观和感觉。我将保持低保真度,以免分散本文的重点。
更新:在撰写本文之后,我了解到微软为 ASP.NET 推出了一个新的成员资格系统,名为 ASP.NET Identity。该系统似乎不依赖于旧的 Membership API。此外,它支持声明。鉴于此,对于您的 ASP.NET 应用程序,它可能是一个值得考虑的身份验证系统。
身份验证 – 准备工作
在示例应用程序中,我将使用 Forms 身份验证。请注意,这并不意味着我必须使用 Membership provider。Membership API 是可用的,但肯定不是强制性的。它只是一个数据存储的抽象。我选择使用 Brock Allen 的库,MembershipReboot,尽管它的名字如此,但它与 Membership provider 无关。事实上,它根本不是一个 provider。它是一个执行大量与身份验证相关的功能的库,包括
- 创建新用户,包括密码的加密/哈希处理
- 验证用户身份
- 启用用户解锁
- 重置用户密码
开始使用 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 脚本。为了方便您,我已将这些脚本包含在本文的下载代码中。您应该按照以下顺序运行这些脚本:
- 201301101956394_InitialMigration.sql
- 201302251410424_ExpandKeyAndAddAccountCloseFields.sql
- 201304152032598_NameId.sql
- 201304160322055_VerificationPurpose.sql
现在我们有了一个数据库(我叫它 MembershipReboot
,这是因为我缺乏想象力),其中包含两个表:
UserAccounts
UserClaims
这是我们所有身份验证相关信息的存储。也就是说,创建用户、删除用户、重置密码、锁定用户、解锁用户、登录用户、登出用户等。
快速跳到创建用户
此时,我们需要向数据库添加一个用户。我们不能仅通过后端简单的 SQL 语句来添加。用户的密码需要正确哈希。鉴于此,我们需要使用某种前端来创建用户,该前端将执行正确的哈希处理。下载代码中的应用程序包含一个专门用于此目的的页面。
因此,我们将绕道而行。要添加用户:
- 在下载的代码中,打开名为 ClaimsAspMvc 的目录中的 Visual Studio 解决方案。
- 运行解决方案,点击页面上唯一的超链接导航到 **创建新用户** 页面。
- 填写表单以添加用户。
- 使用 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 阶段代码),您会发现一个要求您输入消息的页面。在文本框中输入消息并单击按钮。您应该立即被重定向到登录页面。现在我们有了应用程序身份验证代码的最基本起点。
快速查看 AdminController
的 Login
Action 方法。我几乎是从 MembershipReboot
源代码附带的示例应用程序中逐字复制的。请注意,我没有使用 IOC 容器来处理 UserAuthenticationService
或 ClaimsBasedAuthenticationService
的依赖注入,因为我不想分散本文的关键目的。在实际应用中,这些服务会通过 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
行为的一种方式。其他选项包括:
allowAccountDeletion
emailIsUsername
requireAccountVerification
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 如何持久化这些凭据。
如果您现在打开下载代码的第 2 阶段,我们已经使用 MembershipReboot
库实现了基本身份验证。用法如下:
- 首次加载应用程序时,您将作为匿名用户访问它。然后,在文本框中输入一条消息并单击提交按钮。
- 这将把您重定向到登录页面,您可以在其中输入您的登录详细信息。输入您的用户的用户名和密码,您将被带回到原始屏幕。
- 您可以单击“注销”链接进行注销。然后再次登录,以确认一切正常。
注意:有一些很棒的浏览器扩展程序可以清除 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 |
从 AreWeAllowedToDoItManager
的 CheckAccess
方法中,我们知道正在验证的事实是:
Evaluate if the current user with a Messenger claim of ImportantMessenger
is permitted to do a Write action on the ImportantMessage resource
因此,打开下载代码的第 3 阶段,确保您使用的用户已注销(注销他们或清除浏览器 cookie)。然后,按照以下步骤操作:
- 输入消息并单击“提交消息”按钮,然后观察它如何将您转移到登录页面。由于匿名用户没有
CustomClaimTypes.Messenger
声明,因此它无法访问HomeController
上的 Index 操作(HttpPost
版本)。 - 登录。您将被重定向回
Index
页面,并显示 302 请求。 - 在输入框中键入一条消息并单击“提交消息”按钮。您应该会看到消息出现在输入框下方。
成功!已登录的用户已通过授权检查。
您可以尝试通过尝试“提交消息”来测试这一点,但使用已登录但没有 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
。这两种方法在错误消息本身中都得到了非常清晰的解释:
- 第一种方法是向主体 Claims 集合添加以下任一声明:
- http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
- http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider
- 第二种方法是在
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 日 |