本文摘自 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(五)的更多相关文章

  1. C# 多线程学习笔记 - 2

    本文主要针对 GKarch 相关文章留作笔记,仅在原文基础上记录了自己的理解与摘抄部分片段. 遵循原作者的 CC 3.0 协议. 如果想要了解更加详细的文章信息内容,请访问下列地址进行学习. 原文章地 ...

  2. Replication的犄角旮旯(五)--关于复制identity列

    <Replication的犄角旮旯>系列导读 Replication的犄角旮旯(一)--变更订阅端表名的应用场景 Replication的犄角旮旯(二)--寻找订阅端丢失的记录 Repli ...

  3. asp.net identity 2.2.0 中角色启用和基本使用(五)

    建立控制器UsersAdminController 第一步:在controllers文件夹上点右键>添加>控制器, 我这里选的是“MVC5 控制器-空”,名称设置为:UsersAdminC ...

  4. Asp.net core Identity + identity server + angular 学习笔记 (第五篇)

    ABAC (Attribute Based Access Control) 基于属性得权限管理. 属性就是 key and value 表达力非常得强. 我们可以用 key = role value ...

  5. Owin+ASP.NET Identity浅析系列(五)接入第三方登录

    在今天,读书有时是件“麻烦”事.它需要你付出时间,付出精力,还要付出一份心境.--仅以<Owin+ASP.NET Identity浅析系列>来祭奠那逝去的…… OK,用户角色实现后,我们回 ...

  6. Identity角色管理五(添加用户到角色组)

    因需要在用户列表中点详情按钮来到当前页,所以需要展示分组详情,并展示当前所属角色组的用户 public async Task<ActionResult> Details(string id ...

  7. Identity用户管理入门五(登录、注销)

    一.建立LoginViewModel视图模型 using System.ComponentModel.DataAnnotations; namespace Shop.ViewModel { publi ...

  8. Identity Server 4 从入门到落地(五)—— 使用Ajax访问Web Api

    前面的部分: Identity Server 4 从入门到落地(一)-- 从IdentityServer4.Admin开始 Identity Server 4 从入门到落地(二)-- 理解授权码模式 ...

  9. mvc core2.1 Identity.EntityFramework Core 用户列表预览 删除 修改 (五)

    用户列表预览 Controllers->AccountController.cs [HttpGet] public IActionResult Index() { return View(_us ...

随机推荐

  1. window的Navigator 对象

    Navigator 对象包含有关浏览器的信息. Navigator 对象属性 document.write("浏览器的代码名:" + navigator.appCodeName + ...

  2. php curl中x-www-form-urlencoded与multipart/form-data 方式 Post 提交数据详解

    multipart/form-data 方式 post的curl库,模拟post提交的时候,默认的方式 multipart/form-data ,这个算是post提交的几个基础的实现方式. $post ...

  3. 纯小白入手 vue3.0 CLI - 3.2 - 路由的初级使用

    vue3.0 CLI 真小白一步一步入手全教程系列:https://www.cnblogs.com/ndos/category/1295752.html 尽量把纷繁的知识,肢解重组成为可以堆砌的知识. ...

  4. 不用Visual Studio,5分钟轻松实现一张报表

    常规的报表设计,如RDLC.水晶报表等,需要安装Visual Studio,通过VS提供的报表设计界面来设计报表,通过VS设计报表对.NET开发者而言非常方便,但是对于非开发人员,要安装4G的一个VS ...

  5. 【转】解决CentOS 64位系统vsftpd 530 login incorrect的问题

    转自:http://www.centos.bz/2011/12/centos-64-install-vsftpd-530-login-incorredct/ 今天在centos 6 64位测试安装vs ...

  6. UEditor单个图片上传遇到的问题记录

    查看了ueditor.all.js得源代码发现单图片上传是在选择文件输入框change事件执行表单Submit,但是出现一个问题请求头没有加入Cookie,导致后端身份认证失败,上传最终失败. ued ...

  7. 百度纯CSS生成菜单

    首页我们打看dreamweaver或其它编辑器,创建一个名为nav的导航菜单 <div class="nav"> <ul> <li><a ...

  8. LCD显示异常分析——撕裂(tear effect)【转】

    转自:LCD显示异常分析--撕裂(tear effect) 概述 在上一篇<LCD显示异常分析--开机闪现花屏>中,我们一起分析了开机花屏的问题,在这一篇中,我将对LCD撕裂(tear e ...

  9. python中json序列化的东东

    之所以写这个因为自己总是弄混了,容易弄错,记下来有事没事看看   序列化是指把变量从内存中变成可存储或传输的过程称之为序列化用(使用dump或者dumps),把变量内容从序列化的对象重新读到 内存里称 ...

  10. 【 nginx 】怎么安装nginx

    一,下载地址:http://nginx.org/en/download.html 二,下载完成之后,是一个安装包,解压之后就能直接使用 三,点击进去我们刚刚解压好的nginx的安装包,打开nginx. ...