本文摘自 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. 微信小程序 数组索引 data-“”解释

    按照官方最新文档循环的方式,索引值是以  wx:for-index="index" 方式写的, 以   parseInt(event.currentTarget.dataset.i ...

  2. easyUI combobox combotree 模糊查询,带上下键选择功能,待完善。。。。

    /2017年4月9日 11:52:36 /** * combobox和combotree模糊查询 * combotree 结果带两级父节点(手动设置数量) * 键盘上下键选择叶子节点 * 键盘回车键设 ...

  3. ES6模块化与常用功能

    目前开发环境已经普及使用,如vue,react等,但浏览器环境却支持不好,所以需要开发环境编译,下面介绍下开发环境的使用和常用语法: 一,ES6模块化 1,模块化的基本语法 ES6 的模块自动采用严格 ...

  4. 项目开发常见字符串处理模型-strstr-while/dowhile模型

    strstr-whiledowhile模型用于在母字符串中查找符合特征的子字符串. c语言库提供了strstr函数,strstr函数用于判断母字符串中是否包含子字符串,包含的话返回子字符串的位置指针, ...

  5. Django基础篇--Models

    在Django中创建与数据库的链接并调用数据库的数据是很关键的步骤,那么怎么实现这个过程呢? 下面这篇文章简单梳理了一下创建Model层的过程和应用 模型-Models 首先需要理解什么是模型? 模型 ...

  6. 我的简历 PHP Java C# 技术总监

          石先生 ID:303321266 目前正在找工作 13611326258 hr_msn@163.com 男|32 岁 (1985/08/06)|现居住北京-海淀区|12年工作经验     ...

  7. Spring Boot 中配置文件application.properties使用

    一.配置文档配置项的调用(application.properties可放在resources,或者resources下的config文件夹里) package com.my.study.contro ...

  8. EntityFramework Code-First 简易教程(十一)-------从已存在的数据库中映射出表

    怎样从一个已存在的数据库中映射表到 entity 实体? Entity Framework 提供了一个简便方法,可以为已存在的数据库里的所有表和视图创建实体类(entity class),并且可以用 ...

  9. Unity Chan 2D Asset

    Unity Chan 2D Asset 4月份時,UNITY CHAN 官方網站推出了3D大島こはく,之後也有更新1.11版,而在六月12日時,則釋出了2D版本素材,一樣可以在UNITY CHAN 官 ...

  10. [HDFS_add_2] SecondaryNameNode 滚动 NameNode 数据流程

    0. 说明 在 将 SecondaryNameNode 配置到 s105 节点上 的基础上进行 SecondaryNameNode 滚动 NameNode 数据流程 分析 1. SecondaryNa ...