• 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 = "200000")] 
    public string AnotherAction() 
    { 
        return "这也是一个受保护的Action"; 
    } 
  • 相关阅读:
    Java动态绑定和静态绑定-多态
    Java方法内联
    Java反射机制及原理
    Jvm-类加载机制
    Zookeeper 源码解析-环境准备
    SpringMvc源码解析
    Java虚拟机的意义
    起跑线
    js动态生成html,onclick事件失效解决方法
    虚拟机能ping通,但是telnet某个端口却不行
  • 原文地址:https://www.cnblogs.com/Pinapple/p/6841214.html
Copyright © 2020-2023  润新知