Identity(五)
本文摘自 ASP.NET MVC 随想录—— 使用ASP.NET Identity实现基于声明的授权,高级篇
在这篇文章中,我将继续ASP.NET Identity 之旅,这也是ASP.NET Identity 三部曲的最后一篇。在本文中,将为大家介绍ASP.NET Identity 的高级功能,它支持声明式并且还可以灵活的与ASP.NET MVC 授权结合使用,同时,它还支持使用第三方来实现身份验证。
走进声明的世界
在旧的用户管理系统,例如使用了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 = "")]
public string AnotherAction()
{
return "这也是一个受保护的Action";
}
Identity(五)的更多相关文章
- C# 多线程学习笔记 - 2
本文主要针对 GKarch 相关文章留作笔记,仅在原文基础上记录了自己的理解与摘抄部分片段. 遵循原作者的 CC 3.0 协议. 如果想要了解更加详细的文章信息内容,请访问下列地址进行学习. 原文章地 ...
- Replication的犄角旮旯(五)--关于复制identity列
<Replication的犄角旮旯>系列导读 Replication的犄角旮旯(一)--变更订阅端表名的应用场景 Replication的犄角旮旯(二)--寻找订阅端丢失的记录 Repli ...
- asp.net identity 2.2.0 中角色启用和基本使用(五)
建立控制器UsersAdminController 第一步:在controllers文件夹上点右键>添加>控制器, 我这里选的是“MVC5 控制器-空”,名称设置为:UsersAdminC ...
- Asp.net core Identity + identity server + angular 学习笔记 (第五篇)
ABAC (Attribute Based Access Control) 基于属性得权限管理. 属性就是 key and value 表达力非常得强. 我们可以用 key = role value ...
- Owin+ASP.NET Identity浅析系列(五)接入第三方登录
在今天,读书有时是件“麻烦”事.它需要你付出时间,付出精力,还要付出一份心境.--仅以<Owin+ASP.NET Identity浅析系列>来祭奠那逝去的…… OK,用户角色实现后,我们回 ...
- Identity角色管理五(添加用户到角色组)
因需要在用户列表中点详情按钮来到当前页,所以需要展示分组详情,并展示当前所属角色组的用户 public async Task<ActionResult> Details(string id ...
- Identity用户管理入门五(登录、注销)
一.建立LoginViewModel视图模型 using System.ComponentModel.DataAnnotations; namespace Shop.ViewModel { publi ...
- Identity Server 4 从入门到落地(五)—— 使用Ajax访问Web Api
前面的部分: Identity Server 4 从入门到落地(一)-- 从IdentityServer4.Admin开始 Identity Server 4 从入门到落地(二)-- 理解授权码模式 ...
- mvc core2.1 Identity.EntityFramework Core 用户列表预览 删除 修改 (五)
用户列表预览 Controllers->AccountController.cs [HttpGet] public IActionResult Index() { return View(_us ...
随机推荐
- 【读书笔记】iOS-手势识别
一,事件处理机制 事件是当用户手指触及屏幕,或地屏幕上滑动,或摇晃设备等时候,系统不断地把这些事件通过消息发送给应用程序对象.在iOS设备中能够捕获的事件有3种:触摸事件,移动事件和多媒体远程控制事件 ...
- 《Inside C#》笔记(十五) 非托管代码 上
为了保证向后兼容性,C#和.NET可以通过非托管的方式运行旧代码.非托管代码是指没有被.NET运行时管控的代码.非托管代码主要包括:平台调用服务(PlatformInvocation Services ...
- 2018下半年Android面试历程
个人看法:可以总结下他的面试经历以及涉及到的面试题 下面开始正文吧: 从今年下半年以来就开始在杭州准备简历找工作了,原因基本都懂的,没多少工资,投递简历的渠道是Boss,偶尔也在拉钩上投递,刚开始把简 ...
- Android解析XML文件
XML文件和获取XML值 XML文件样例 <?xml version="1.0" encoding="utf-8"?> <citys> ...
- 浅谈Arrays.asList()方法的使用
首先,该方法是将数组转化为list.有以下几点需要注意: (1)该方法不适用于基本数据类型(byte,short,int,long,float,double,boolean) (2)该方法将数组与列表 ...
- C++ 获取当前正在执行的函数的相关信息
(我的运行环境:win10x64+vs2015通过, 有的环境KUbuntu 8.04.1 x64 g++ 4.2.3也通过了)主要通过宏来实现:(注意,开头和结尾都是两个下划线) 1. __PRET ...
- scrapy之spider模块
scrapy中的spider的用法 : 1.scrapy命令行可以传参数给构造器 scrapy crawl myspider -a category=electronics 构造器接收传入的参数 im ...
- windows防火墙安全设置指定ip访问指定端口
场景摘要: 1.我有三台腾讯云服务器 2.我日常办公网络的ip换了 3.我在腾讯云上面改了安全规则,也不能访问我A服务器的21,1433等端口 4.开始我以为是办公网络的安全设置问题 5.我进B服务器 ...
- Django框架的简介
Django框架的背景 Django是一款基于Python开发的全栈式一体化Web 应用框架.2003 年问世之初,它只是 美国一家报社的内部工具,2005 年 7 月使用 BSD 许可证完成了开源. ...
- Windows端部署zabbix-agent
一.windows客户端的配置关闭windows防火墙或者开通10050和10051端口(1).关闭防火墙(不推荐直接关闭,测试可以这样做,尤其是最近勒索病毒猛烈)开始—控制面板—windows防火墙 ...