ASP.NET Identity
使用ASP.NET Identity实现基于声明的授权
阅读目录
在这篇文章中,我将继续ASP.NET Identity 之旅,这也是ASP.NET Identity 三部曲的最后一篇。在本文中,将为大家介绍ASP.NET Identity 的高级功能,它支持声明式并且还可以灵活的与ASP.NET MVC 授权结合使用,同时,它还支持使用第三方来实现身份验证。
关于ASP.NET Identity 的基础知识,请参考如下文章:
ASP.NET MVC 随想录——开始使用ASP.NET Identity,初级篇
ASP.NET MVC 随想录——探索ASP.NET Identity 身份验证和基于角色的授权,中级篇
本文的示例,你可以在此下载和预览:
走进声明的世界
在旧的用户管理系统,例如使用了ASP.NET Membership的应用程序,我们的应用程序被认为是获取用户所有信息的权威来源,所以本质上可以将应用程序视为封闭的系统,它包含了所有的用户信息。在上一篇文章中,我使用ASP.NET Identity 验证用户存储在数据库的凭据,并根据与这些凭据相关联的角色进行授权访问,所以本质上身份验证和授权所需要的用户信息来源于我们的应用程序。
ASP.NET Identity 还支持使用声明来和用户打交道,它效果很好,而且应用程序并不是用户信息的唯一来源,有可能来自外部,这比传统角色授权来的更为灵活和方便。
接下来我将为大家介绍ASP.NET Identity 是如何支持基于声明的授权(claims-based authorization)。
1.理解什么是声明
声明(Claims)其实就是用户相关的一条一条信息的描述,这些信息包括用户的身份(如Name、Email、Country等)和角色成员,而且,它描述了这些信息的类型、值以及发布声明的认证方等。我们可以使用声明来实现基于声明的授权。声明可以从外部系统获得,当然也可以从本地用户数据库获取。
对于ASP.NET MVC应用程序,通过自定义AuthorizeAttribute,声明能够被灵活的用来对指定的Action 方法授权访问,不像传统的使用角色授权那么单一,基于声明的授权更加丰富和灵活,它允许使用用户信息来驱动授权访问。
既然声明(Claim)是一条关于用户信息的描述,最简单的方式来阐述什么是声明就是通过具体的例子来展示,这比抽象概念的讲解来的更有用。所以,我在示例项目中添加了一个名为Claims 的 Controller,它的定义如下所示:
- public class ClaimsController : Controller
- {
- [Authorize]
- public ActionResult Index()
- {
- ClaimsIdentity claimsIdentity = HttpContext.User.Identity as ClaimsIdentity;
- if (claimsIdentity == null)
- {
- return View("Error", new string[] {"未找到声明"});
- }
- else
- {
- return View(claimsIdentity.Claims);
- }
- }
- }
在这个例子中可以看出ASP.NET Identity 已经很好的集成到ASP.NET 平台中,而HttpContext.User.Identity 属性返回一个 IIdentity 接口的实现,而当与ASP.NET Identity 结合使用时,返回的是ClaimsIdentity 对象。
ClaimsIdentity 类被定义在System.Security.Claims 名称空间下,它包含如下重要的成员:
Claims |
返回用户包含的声明对象集合 |
AddClaim(claim) |
为用户添加一个声明 |
AddClaims(claims) |
为用户添加一系列声明 |
HasClaim(predicate) |
判断是否包含声明,如果是,返回True |
RemoveClaim(claim) |
为用户移除声明 |
当然ClaimsIdentity 类还有更多的成员,但上述表描述的是在Web应用程序中使用频率很高的成员。在上述代码中,将HttpContext.User.Identity 转换为ClaimsIdentity 对象,并通过该对象的Claims 属性获取到用户相关的所有声明。
一个声明对象代表了用户的一条单独的信息数据,声明对象包含如下属性:
Issuer |
返回提供声明的认证方名称 |
Subject |
返回声明指向的ClaimIdentity 对象 |
Type |
返回声明代表的信息类型 |
Value |
返回声明代表的用户信息的值 |
有了对声明的基本概念,对上述代码的View进行修改,它呈现用户所有声明信息,相应的视图代码如下所示:
- @using System.Security.Claims
- @using Users.Infrastructure
- @model IEnumerable<Claim>
- @{
- ViewBag.Title = "Index";
- }
- <div class="panel panel-primary">
- <div class="panel-heading">
- 声明
- </div>
- <table class="table table-striped">
- <tr>
- <th>Subject</th>
- <th>Issuer</th>
- <th>Type</th>
- <th>Value</th>
- </tr>
- @foreach (Claim claim in Model.OrderBy(x=>x.Type))
- {
- <tr>
- <td>@claim.Subject.Name</td>
- <td>@claim.Issuer</td>
- <td>@Html.ClaimType(claim.Type)</td>
- <td>@claim.Value</td>
- </tr>
- }
- </table>
- </div>
Claim对象的Type属性返回URI Schema,这对于我们来说并不是特别有用,常见的被用来当作值的Schema定义在System.Security.Claims.ClaimType 类中,所以要使输出的内容可读性更强,我添加了一个HTML helper,它用来格式化Claim.Type 的值:
- public static MvcHtmlString ClaimType(this HtmlHelper html, string claimType)
- {
- FieldInfo[] fields = typeof(ClaimTypes).GetFields();
- foreach (FieldInfo field in fields)
- {
- if (field.GetValue(null).ToString() == claimType)
- {
- return new MvcHtmlString(field.Name);
- }
- }
- return new MvcHtmlString(string.Format("{0}",
- claimType.Split('/', '.').Last()));
- }
有了上述的基础设施代码后,我请求ClaimsController 下的Index Action时,显示用户关联的所有声明,如下所示:
创建并使用声明
有两个原因让我觉得声明很有趣。第一个原因是,应用程序能从多个来源获取声明,而不是仅仅依靠本地数据库来获取。在稍后,我会向你展示如何使用外部第三方系统来验证用户身份和创建声明,但此时我添加一个类,来模拟一个内部提供声明的系统,将它命名为LocationClaimsProvider,如下所示:
- public static class LocationClaimsProvider
- {
- public static IEnumerable<Claim> GetClaims(ClaimsIdentity user)
- {
- List<Claim> claims=new List<Claim>();
- if (user.Name.ToLower()=="admin")
- {
- claims.Add(CreateClaim(ClaimTypes.PostalCode, "DC 20500"));
- claims.Add(CreateClaim(ClaimTypes.StateOrProvince, "DC"));
- }
- else
- {
- claims.Add(CreateClaim(ClaimTypes.PostalCode, "NY 10036"));
- claims.Add(CreateClaim(ClaimTypes.StateOrProvince, "NY"));
- }
- return claims;
- }
- private static Claim CreateClaim(string type,string value)
- {
- return new Claim(type, value, ClaimValueTypes.String, "RemoteClaims");
- }
- }
上述代码中,GetClaims 方法接受一个参数为ClaimsIdentity 对象并为用户创建了PostalCode和StateOrProvince的声明。在这个类中,假设我模拟一个系统,如一个中央的人力资源数据库,那么这将是关于工作人员本地信息的权威来源。
声明是在身份验证过程被添加到用户中,故在Account/Login Action对代码稍作修改:
- [HttpPost]
- [AllowAnonymous]
- [ValidateAntiForgeryToken]
- public async Task<ActionResult> Login(LoginModel model,string returnUrl)
- {
- if (ModelState.IsValid)
- {
- AppUser user = await UserManager.FindAsync(model.Name, model.Password);
- if (user==null)
- {
- ModelState.AddModelError("","无效的用户名或密码");
- }
- else
- {
- var claimsIdentity =
- await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
- claimsIdentity.AddClaims(LocationClaimsProvider.GetClaims(claimsIdentity));
- AuthManager.SignOut();
- AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, claimsIdentity);
- return Redirect(returnUrl);
- }
- }
- ViewBag.returnUrl = returnUrl;
- return View(model);
- }
修改完毕,运行应用程序,身份验证成功过后,浏览Claims/Index 地址,你就可以看到已经成功对用户添加声明了,如下截图所示:
获取声明来自多个来源意味着我们的应用程序不会有重复数据并可以和外部数据集成。Claim 对象的Issuer 属性 告诉你这个声明的来源,这能帮助我们精确判断数据的来源。举个例子,从中央人力资源数据库获取的信息比从外部供应商邮件列表获取的信息会更准确。
声明是有趣的第二个原因是你能用他们来管理用户访问,这比使用标准的角色控制来的更为灵活。在前一篇文章中,我创建了一个专门负责角色的管理RoleContoller,在RoleController里实现用户和角色的绑定,一旦用户被赋予了角色,则该成员将一直隶属于这个角色直到他被移除掉。这会有一个潜在的问题,在大公司工作时间很长的员工,当他们换部门时换工作时,如果旧的角色没被删除,那么可能会出现资料泄露的风险。
考虑使用声明吧,如果把传统的角色控制视为静态的话,那么声明是动态的,我们可以在程序运行时动态创建声明。声明可以直接基于已知的用户信息来授权用户访问,这样确保当声明数据更改时授权也更改。
最简单的是使用Role 声明来对Action 受限访问,这我们已经很熟悉了,因为ASP.NET Identity 已经很好的集成到了ASP.NET 平台中了,当使用ASP.NET Identity 时,HttpContext.User 返回的是ClaimsPrincipal 对象,它实现了IsInRole 方法并使用HasClaim来判断指定的角色声明是否存在,从而达到授权。
接着刚才的话题,我们想让授权是动态的,是由用户信息(声明)驱动的,所以我创建了一个ClaimsRoles类,用来模拟生成声明,如下所示:
- public class ClaimsRoles
- {
- public static IEnumerable<Claim> CreateRolesFromClaims(ClaimsIdentity user)
- {
- List<Claim> claims = new List<Claim>();
- if (user.HasClaim(x => x.Type == ClaimTypes.StateOrProvince
- && x.Issuer == "RemoteClaims" && x.Value == "北京")
- && user.HasClaim(x => x.Type == ClaimTypes.Role
- && x.Value == "Employee"))
- {
- claims.Add(new Claim(ClaimTypes.Role, "BjStaff"));
- }
- return claims;
- }
- }
初略看一下CreateRolesFromClaims方法中的代码,使用Lambda表达式检查用户是否有来自Issuer为RemoteClaims ,值为北京的StateOrProvince声明和值为Employee 的Role声明,如果用户都包含两者,新增一个值为BjStaff 的 Role 声明。最后在Login Action 时调用此方法,如下所示:
- [HttpPost]
- [AllowAnonymous]
- [ValidateAntiForgeryToken]
- public async Task<ActionResult> Login(LoginModel model,string returnUrl)
- {
- if (ModelState.IsValid)
- {
- AppUser user = await UserManager.FindAsync(model.Name, model.Password);
- if (user==null)
- {
- ModelState.AddModelError("","无效的用户名或密码");
- }
- else
- {
- var claimsIdentity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
- claimsIdentity.AddClaims(LocationClaimsProvider.GetClaims(claimsIdentity));
- claimsIdentity.AddClaims(ClaimsRoles.CreateRolesFromClaims(claimsIdentity));
- AuthManager.SignOut();
- AuthManager.SignIn(new AuthenticationProperties {IsPersistent = false}, claimsIdentity);
- return Redirect(returnUrl);
- }
- }
- ViewBag.returnUrl = returnUrl;
- return View(model);
- }
现在就可以基于角色为BjStaff对OtherAction受限访问,如下所示:
- [Authorize(Roles = "BjStaff")]
- public string OtherAction()
- {
- return "这是一个受保护的Action";
- }
当用户信息发生改变时,如若生成的声明不为BjStaff,那么他也就没权限访问OtherAction了,这完全是由用户信息所驱动,而非像传统的在RoleController中显示修改用户和角色的关系。
基于声明的授权
在前一个例子中证明了如何使用声明来授权,但是这有点不直接因为我基于声明来产生角色然后再基于新的角色来授权。一个更加直接和灵活的方法是通过创建一个自定义的授权过滤器特性来实现,如下展示:
- public class ClaimsAccessAttribute:AuthorizeAttribute
- {
- public string Issuer { get; set; }
- public string ClaimType { get; set; }
- public string Value { get; set; }
- protected override bool AuthorizeCore(HttpContextBase context)
- {
- return context.User.Identity.IsAuthenticated
- && context.User.Identity is ClaimsIdentity
- && ((ClaimsIdentity)context.User.Identity).HasClaim(x =>
- x.Issuer == Issuer && x.Type == ClaimType && x.Value == Value
- );
- }
- }
ClaimsAccessAttribute 特性继承自AuthorizeAttribute,并Override了 AuthorizeCore 方法,里面的业务逻辑是当用户验证成功并且IIdentity的实现是ClaimsIdentity 对象,同时用户包含通过属性传入的声明,最后将此Attribute 放在AnOtherAction 前,如下所示:
- [ClaimsAccess(Issuer = "RemoteClaims", ClaimType = ClaimTypes.PostalCode, Value = "200000")]
- public string AnotherAction()
- {
- return "这也是一个受保护的Action";
- }
使用第三方来身份验证
像ASP.NET Identity 这类基于声明的系统的一个好处是任何声明能从外部系统获取,这意味着其他应用程序能帮我们来身份验证。ASP.NET Identity 基于这个原则增加对第三方如Google、Microsoft、FaceBook身份验证的支持。
使用第三方身份验证有许多好处:许多用户已经有一个第三方账户了,并且你也不想在这个应用程序管理你的凭据。用户也不想在每一个网站上注册账户并都记住密码。使用一个统一的账户会比较灵活。
1.启用Google 账户身份验证
ASP.NET Identity 发布了对第三方身份验证的支持,通过Nuget来安装:
Install-Package Microsoft.Owin.Security.Google
当Package 安装完成后,在OWIN Startup启动项中,添加对身份验证服务的支持:
- app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
- //http://www.asp.net/mvc/overview/security/create-an-aspnet-mvc-5-app-with-facebook-and-google-oauth2-and-openid-sign-on
- app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
- {
- ClientId = "165066370005-6nhsp87llelff3tou91hhktg6eqgr0ke.apps.googleusercontent.com",
- ClientSecret = "euWbCSUZujjQGKMqOyz0msbq",
- });
在View中,添加一个通过Google 登陆的按钮:
- @using (Html.BeginForm("GoogleLogin", "Account"))
- {
- <input type="hidden" name="returnUrl" value="@ViewBag.returnUrl" />
- <button class="btn btn-primary" type="submit">Google 账户登录 </button>
- }
当点击按钮时,Post到Account/GoogleLogin :
- [HttpPost]
- [AllowAnonymous]
- public ActionResult GoogleLogin(string returnUrl)
- {
- var properties = new AuthenticationProperties
- {
- RedirectUri = Url.Action("GoogleLoginCallback",
- new { returnUrl = returnUrl })
- };
- HttpContext.GetOwinContext().Authentication.Challenge(properties, "Google");
- return new HttpUnauthorizedResult();
- }
GoogleLogin 方法创建了AuthenticationProperties 类型的对象,并制定RedirectUri为当前Controller下的GoogleLoginCallBack Action,接下来就是见证奇迹的时候,返回401 Unauthorize 然后OWIN 中间件重定向到Google 登陆页面,而不是默认的Account/Login。这意味着,当用户点击以Google登陆按钮后,浏览器重定向到Google 身份验证服务然后一旦身份验证通过,重定向到GoogleLoginCallBack:
- /// <summary>
- /// Google登陆成功后(即授权成功)回掉此Action
- /// </summary>
- /// <param name="returnUrl"></param>
- /// <returns></returns>
- [AllowAnonymous]
- public async Task<ActionResult> GoogleLoginCallback(string returnUrl)
- {
- ExternalLoginInfo loginInfo = await AuthManager.GetExternalLoginInfoAsync();
- AppUser user = await UserManager.FindAsync(loginInfo.Login);
- if (user == null)
- {
- user = new AppUser
- {
- Email = loginInfo.Email,
- UserName = loginInfo.DefaultUserName,
- City = Cities.Shanghai,
- Country = Countries.China
- };
- IdentityResult result = await UserManager.CreateAsync(user);
- if (!result.Succeeded)
- {
- return View("Error", result.Errors);
- }
- result = await UserManager.AddLoginAsync(user.Id, loginInfo.Login);
- if (!result.Succeeded)
- {
- return View("Error", result.Errors);
- }
- }
- ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user,
- DefaultAuthenticationTypes.ApplicationCookie);
- ident.AddClaims(loginInfo.ExternalIdentity.Claims);
- AuthManager.SignIn(new AuthenticationProperties
- {
- IsPersistent = false
- }, ident);
- return Redirect(returnUrl ?? "/");
- }
对上述代码中,通过AuthManager.GetExternalLoginInfoAsync 方法获取外部登陆详细信息,ExternalLoginInfo 类定义了如下属性:
DefaultUserName |
返回用户名 |
|
返回Email 地址 |
ExternalIdentity |
返回代表用户的ClaimIdentity |
Login |
返回一个UserLoginInfo用来描述外部登陆 |
接着使用定义在UserManager对象中的FindAsync方法,传入ExternalLoginInfo.Login 属性,来获取AppUser对象,如果返回的对象不存在,这意味这这是该用户第一次登录到我们的应用程序中,所以我创建了一个AppUser对象并填充了属性然后将其保存到数据库中。
我同样也保存了用户登陆的详细信息以便下一次能找到。
最后,创建ClaimsIdentity 对象并创建Cookie,让应用程序知道用户已经验证通过了。
为了测试Google 身份验证,我们启动应用程序,当验证通过后,访问Claims/Index,得到如下声明:
可以看到一些声明的认证发布者是Google,而且这些信息来自于第三方。
小节
在这篇文章中,我为大家介绍了ASP.NET Identity 支持的一些高级功能,并解释了Claim是如何运行以及怎样创建灵活的授权访问。在本文最后演示了如和通过Google来身份验证。
在技术领域,我们往往会对一些晦涩难翻译的术语感到惶恐,甚至会排斥它,比如yield、Identity、Claim。
在夜生人静时,泡一壶茶,拿上一本书,细细品读,或许会有别样的精彩正等在我们。
ASP.NET Identity的更多相关文章
- 从Membership 到 .NET4.5 之 ASP.NET Identity
我们前面已经讨论过了如何在一个网站中集成最基本的Membership功能,然后深入学习了Membership的架构设计.正所谓从实践从来,到实践从去,在我们把Membership的结构吃透之后,我们要 ...
- MVC5 - ASP.NET Identity登录原理 - Claims-based认证和OWIN
在Membership系列的最后一篇引入了ASP.NET Identity,看到大家对它还是挺感兴趣的,于是来一篇详解登录原理的文章.本文会涉及到Claims-based(基于声明)的认证,我们会详细 ...
- ASP.NET Identity入门系列教程(一) 初识Identity
摘要 通过本文你将了解ASP.NET身份验证机制,表单认证的基本流程,ASP.NET Membership的一些弊端以及ASP.NET Identity的主要优势. 目录 身份验证(Authentic ...
- [转]MVC5 - ASP.NET Identity登录原理 - Claims-based认证和OWIN
本文转自:http://www.cnblogs.com/jesse2013/p/aspnet-identity-claims-based-authentication-and-owin.html 在M ...
- Configuring Autofac to work with the ASP.NET Identity Framework in MVC 5
https://developingsoftware.com/configuring-autofac-to-work-with-the-aspnet-identity-framework-in-mvc ...
- asp.net identity UserSecurityStamp 的作用
UserSecurityStamp 主要是用来对用户安全相关信息做一个快照. 在使用asp.net identity 的 CreateAsync(TUser user) 创建一个用户的时候,如果开启了 ...
- ASP.NET Identity V2
Microsoft.AspNet.Identity是微软在MVC 5.0中新引入的一种membership框架,和之前ASP.NET传统的membership以及WebPage所带来的SimpleMe ...
- ASP.NET Identity 2新增双重认证、帐号锁定、防伪印章功能并修复了一些bug
Microsoft最近发布了ASP.NET Identity 2,该版本支持双重认证.帐号锁定以及防伪印章功能,还增强了用户帐号和索引.此外新版本还包含一个改进的密码验证器并修复了一些bug. 借助于 ...
- VS2013中web项目中自动生成的ASP.NET Identity代码思考
vs2013没有再分webform.mvc.api项目,使用vs2013创建一个web项目模板选MVC,身份验证选个人用户账户.项目会生成ASP.NET Identity的一些代码.这些代码主要在Ac ...
- 向空项目添加 ASP.NET Identity
安装 AspNet.Identity 程序包 Microsoft.AspNet.Identity.Core 包含 ASP.NET Identity 核心接口Microsoft.AspNet.Ident ...
随机推荐
- POJ1201 差分约束
给定ai,bi, ci 表示区间[ai,bi]内至少有ci个点, 要求对于所有给定的ai,bi,ci, 至少多少个点才能满足题目的条件 重做这一题学到的一点是, 可以设变量来表示一些东西,然后才能找 ...
- WPF换肤之二:可拉动的窗体
原文:WPF换肤之二:可拉动的窗体 让我们接着上一章: WPF换肤之一:创建圆角窗体 来继续. 在这一章,我主要是实现对圆角窗体的拖动,改变大小功能. 拖动自绘窗体的步骤 首先,通过上节的设计,我们知 ...
- android一些面试题目
1.ListView怎么提高滑动效率 2.说下你做过项目的包的构架,(联网,解析,activity,database) 重点 3.载入大量图片怎么做(包含小图和查看大图) 怎么降低一次跟server的 ...
- Python学习路径8——Python对象2
1.标准型运营商 1.1对象值对照 比较运算符用于如果相同类型的对象是相等.所有的内建类型的是在比较操作中支持,返回布尔比较操作值True 或 False. <span style=" ...
- Cluster Table
对簇表来说,总是要先创建簇段(cluster segment).然后将表关联到cluster segment里.由此可知,簇表也是虚拟表,没有对应的segment,簇表对应的是cluster segm ...
- [Python]How to handle the exception in Python?
This post demonstrates how to use try clause to handle the exceptions def test_exception(case=None): ...
- T-SQL基础(4) - 子查询
简单子查询select * from (select custid, companyname from Sales.Customers where country = N'USA') as USACu ...
- ShareSDK for Android 2.3.8它已发表
ShareSDK for Android 2.3.8已经公布,本次更新内容包含: 1.一键分享加入"摇一摇截图分享"功能 3.优化一键分享截图分享功能 4.一键分享编辑页界面微调 ...
- UVA How Big Is It?
题目例如以下: How Big Is It? Ian's going to California, and he has to pack his things, including hiscolle ...
- A*算法进入
作者文章链接:http://www.policyalmanac.org/games/aStarTutorial.htm 启示式搜索:启示式搜索就是在状态空间中的搜索对每个搜索的位置进行评估,得到最好的 ...