IdentityServer4实战 - 基于角色的权限控制及Claim详解

一.前言

大家好,许久没有更新博客了,最近从重庆来到了成都,换了个工作环境,前面都比较忙没有什么时间,这次趁着清明假期有时间,又可以分享一些知识给大家。在QQ群里有许多人都问过IdentityServer4怎么用Role(角色)来控制权限呢?还有关于Claim这个是什么呢?下面我带大家一起来揭开它的神秘面纱!

二.Claim详解

我们用过IdentityServer4或者熟悉ASP.NET Core认证的都应该知道有Claim这个东西,Claim我们通过在线翻译有以下解释:

(1)百度翻译

(2)谷歌翻译

这里我理解为声明,我们每个用户都有多个Claim,每个Claim声明了用户的某个信息比如:Role=Admin,UserID=1000等等,这里Role,UserID每个都是用户的Claim,都是表示用户信息的单元 ,我们不妨把它称为用户信息单元 。

建议阅读杨总的Claim相关的解析 http://www.cnblogs.com/savorboard/p/aspnetcore-identity.html

三.测试环境中添加角色Claim

这里我们使用IdentityServer4的QuickStart中的第二个Demo:ResourceOwnerPassword来进行演示(代码地址放在文末),所以项目的创建配置就不在这里演示了。

这里我们需要自定义IdentityServer4(后文简称id4)的验证逻辑,然后在验证完毕之后,将我们自己需要的Claim加入验证结果。便可以向API资源服务进行传递。id4定义了IResourceOwnerPasswordValidator接口,我们实现这个接口就行了。

Id4为我们提供了非常方便的In-Memory测试支持,那我们在In-Memory测试中是否可以实现自定义添加角色Claim呢,答案当时是可以的。

1.首先我们需要在定义TestUser测试用户时,定义用户Claims属性,意思就是为我们的测试用户添加额外的身份信息单元,这里我们添加角色身份信息单元:

new TestUser
{SubjectId = "1",Username = "alice",Password = "password",Claims = new List<Claim>(){new Claim(JwtClaimTypes.Role,"superadmin") }
},new TestUser
{SubjectId = "2",Username = "bob",Password = "password",Claims = new List<Claim>(){new Claim(JwtClaimTypes.Role,"admin") }
}

JwtClaimTypes是一个静态类在IdentityModel程序集下,里面定义了我们的jwt token的一些常用的Claim,JwtClaimTypes.Role是一个常量字符串public const string Role = "role";如果JwtClaimTypes定义的Claim类型没有我们需要的,那我们直接写字符串即可。

2.分别启动 QuickstartIdentityServer、Api、ResourceOwnerClient 查看 运行结果:

可以看见我们定义的API资源通过HttpContext.User.Claims并没有获取到我们为测试用户添加的Role Claim,那是因为我们为API资源做配置。

3.配置API资源需要的Claim

在QuickstartIdentityServer项目下的Config类的GetApiResources做出如下修改:

public static IEnumerable<ApiResource> GetApiResources(){  
 return new List<ApiResource>{//                new ApiResource("api1", "My API")new ApiResource("api1", "My API",new List<string>(){JwtClaimTypes.Role})}; }

我们添加了一个Role Claim,现在再次运行(需要重新QuickstartIdentityServer方可生效)查看结果。

可以看到,我们的API服务已经成功获取到了Role Claim。

这里有个疑问,为什么需要为APIResource配置Role Claim,我们的API Resource才能获取到呢,我们查看ApiResource的源码:

public ApiResource(string name, string displayName, IEnumerable<string> claimTypes){    if (name.IsMissing()) throw new ArgumentNullException(nameof(name));Name = name;DisplayName = displayName;Scopes.Add(new Scope(name, displayName));    if (!claimTypes.IsNullOrEmpty()){        foreach (var type in claimTypes){UserClaims.Add(type);}}
}

从上面的代码可以分析出,我们自定义的Claim添加到了一个名为UserClaims的属性中,查看这个属性:

/// <summary>/// List of accociated user claims that should be included when this resource is requested./// </summary>public ICollection<string> UserClaims { get; set; } = new HashSet<string>();

根据注释我们便知道了原因:请求此资源时应包含的相关用户身份单元信息列表。

四.通过角色控制API访问权限

我们在API项目下的IdentityController做出如下更改

[Route("[controller]")]    public class IdentityController : ControllerBase{[Authorize(Roles = "superadmin")][HttpGet]    public IActionResult Get()    {        return new JsonResult(from c in HttpContext.User.Claims select new { c.Type, c.Value });}[Authorize(Roles = "admin")][Route("{id}")][HttpGet]    public string Get(int id)    {        return id.ToString();}
}

我们定义了两个API通过Authorize特性赋予了不同的权限(我们的测试用户只添加了一个角色,通过访问具有不同角色的API来验证是否能通过角色来控制)

我们在ResourceOwnerClient项目下,Program类最后添加如下代码:

response = await client.GetAsync("http://localhost:5001/identity/1");if (!response.IsSuccessStatusCode)
{Console.WriteLine(response.StatusCode);Console.WriteLine("没有权限访问 http://localhost:5001/identity/1");
}else{    var content = response.Content.ReadAsStringAsync().Result;Console.WriteLine(content);
}

这里我们请求第二个API的代码,正常情况应该会没有权限访问的(我们使用的用户只具有superadmin角色,而第二个API需要admin角色),运行一下:

可以看到提示我们第二个,无权访问,正常。

五.如何使用已有用户数据自定义Claim

我们前面的过程都是使用的TestUser来进行测试的,那么我们正式使用时肯定是使用自己定义的用户(从数据库中获取),这里我们可以实现IResourceOwnerPasswordValidator接口,来定义我们自己的验证逻辑。

/// <summary>/// 自定义 Resource owner password 验证器/// </summary>public class CustomResourceOwnerPasswordValidator: IResourceOwnerPasswordValidator{    /// <summary>/// 这里为了演示我们还是使用TestUser作为数据源,/// 正常使用此处应当传入一个 用户仓储 等可以从/// 数据库或其他介质获取我们用户数据的对象/// </summary>private readonly TestUserStore _users;    private readonly ISystemClock _clock;    public CustomResourceOwnerPasswordValidator(TestUserStore users, ISystemClock clock)    {_users = users;_clock = clock;}    /// <summary>/// 验证/// </summary>/// <param name="context"></param>/// <returns></returns>public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)    {        //此处使用context.UserName, context.Password 用户名和密码来与数据库的数据做校验if (_users.ValidateCredentials(context.UserName, context.Password)){            var user = _users.FindByUsername(context.UserName);            //验证通过返回结果 //subjectId 为用户唯一标识 一般为用户id//authenticationMethod 描述自定义授权类型的认证方法 //authTime 授权时间//claims 需要返回的用户身份信息单元 此处应该根据我们从数据库读取到的用户信息 添加Claims 如果是从数据库中读取角色信息,那么我们应该在此处添加 此处只返回必要的Claimcontext.Result = new GrantValidationResult(user.SubjectId ?? throw new ArgumentException("Subject ID not set", nameof(user.SubjectId)),OidcConstants.AuthenticationMethods.Password, _clock.UtcNow.UtcDateTime,user.Claims);}        else{            //验证失败context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "invalid custom credential");}        return Task.CompletedTask;}

在Startup类里配置一下我们自定义的验证器:

实现了IResourceOwnerPasswordValidator还不够,我们还需要实现IProfileService接口,他是专门用来装载我们需要的Claim信息的,比如在token创建期间和请求用户信息终结点是会调用它的GetProfileDataAsync方法来根据请求需要的Claim类型,来为我们装载信息,下面是一个简单实现:

public class CustomProfileService: IProfileService{/// <summary>/// The logger/// </summary>protected readonly ILogger Logger;/// <summary>/// The users/// </summary>protected readonly TestUserStore Users;/// <summary>/// Initializes a new instance of the <see cref="TestUserProfileService"/> class./// </summary>/// <param name="users">The users.</param>/// <param name="logger">The logger.</param>public CustomProfileService(TestUserStore users, ILogger<TestUserProfileService> logger){Users = users;Logger = logger;
}/// <summary>/// 只要有关用户的身份信息单元被请求(例如在令牌创建期间或通过用户信息终点),就会调用此方法/// </summary>/// <param name="context">The context.</param>/// <returns></returns>public virtual Task GetProfileDataAsync(ProfileDataRequestContext context){context.LogProfileRequest(Logger);    //判断是否有请求Claim信息if (context.RequestedClaimTypes.Any()){        //根据用户唯一标识查找用户信息var user = Users.FindBySubjectId(context.Subject.GetSubjectId());        if (user != null){            //调用此方法以后内部会进行过滤,只将用户请求的Claim加入到 context.IssuedClaims 集合中 这样我们的请求方便能正常获取到所需Claimcontext.AddRequestedClaims(user.Claims);}}context.LogIssuedClaims(Logger);    return Task.CompletedTask;
}/// <summary>/// 验证用户是否有效 例如:token创建或者验证/// </summary>/// <param name="context">The context.</param>/// <returns></returns>public virtual Task IsActiveAsync(IsActiveContext context){Logger.LogDebug("IsActive called from: {caller}", context.Caller);    var user = Users.FindBySubjectId(context.Subject.GetSubjectId());context.IsActive = user?.IsActive == true;    return Task.CompletedTask;
}

同样在Startup类里启用我们自定义的ProfileService :AddProfileService<CustomProfileService>()

值得注意的是如果我们直接将用户的所有Claim加入 context.IssuedClaims集合,那么用户所有的Claim都将会无差别返回给请求方。比如默认情况下请求用户终结点(http://Identityserver4地址/connect/userinfo)只会返回sub(用户唯一标识)信息,如果我们在此处直接 context.IssuedClaims=User.Claims,那么所有Claim都将被返回,而不会根据请求的Claim来进行筛选,这样做虽然省事,但是损失了我们精确控制的能力,所以不推荐。

上述说明配图:

如果直接 context.IssuedClaims=User.Claims,那么返回结果如下:

         /// <summary>/// 只要有关用户的身份信息单元被请求(例如在令牌创建期间或通过用户信息终点),就会调用此方法/// </summary>/// <param name="context">The context.</param>/// <returns></returns>public virtual Task GetProfileDataAsync(ProfileDataRequestContext context)        {            var user = Users.FindBySubjectId(context.Subject.GetSubjectId());            if (user != null)context.IssuedClaims .AddRange(user.Claims);            return Task.CompletedTask;}

用户的所有Claim都将被返回。这样降低了我们控制的能力,我们可以通过下面的方法来实现同样的效果,但却不会丢失控制的能力。

(1).自定义身份资源资源

身份资源的说明:身份资源也是数据,如用户ID,姓名或用户的电子邮件地址。 身份资源具有唯一的名称,您可以为其分配任意身份信息单元(比如姓名、性别、身份证号和有效期等都是身份证的身份信息单元)类型。 这些身份信息单元将被包含在用户的身份标识(Id Token)中。 客户端将使用scope参数来请求访问身份资源。

public static IEnumerable<IdentityResource> GetIdentityResourceResources(){    var customProfile = new IdentityResource(name: "custom.profile",displayName: "Custom profile",claimTypes: new[] { "role"});    return new List<IdentityResource>{        new IdentityResources.OpenId(), new IdentityResources.Profile(),customProfile};
}

(2).配置Scope
通过上面的代码,我们自定义了一个名为“customProfile“的身份资源,他包含了"role" Claim(可以包含多个Claim),然后我们还需要配置Scope,我们才能访问到:

new Client
{ClientId = "ro.client",AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,ClientSecrets = {        new Secret("secret".Sha256())},AllowedScopes = { "api1" ,IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile,"custom.profile"}
}

我们在Client对象的AllowedScopes属性里加入了我们刚刚定义的身份资源,下载访问用户信息终结点将会得到和上面一样的结果。

六.总结

写这篇文章,简单分析了一下相关的源码,如果因为有本文描述不清楚或者不明白的地方建议阅读一下源码,或者加下方QQ群在群内提问。如果我们的根据角色的权限认证没有生效,请检查是否正确获取到了角色的用户信息单元。我们需要接入已有用户体系,只需实现IProfileServiceIResourceOwnerPasswordValidator接口即可,并且在Startup配置Service时不再需要AddTestUsers,因为将使用我们自己的用户信息。

Demo地址:https://github.com/stulzq/BlogDemos/tree/master/Id4RoleAndClaim

相关文章:

原文:https://www.cnblogs.com/stulzq/p/8726002.html


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/321753.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

11、java中的I/O流(1)

我对于流的理解是这样的&#xff0c;计算机的本质本来就是对输入的数据进行操作&#xff0c;然后将结果输出的一种工具&#xff0c;数据在各个数据源节点之间进行流动&#xff0c;感觉流就是对这种状态的一种抽象&#xff0c;一个数据流表示的就是一系列数据序列&#xff0c;ja…

ASP.NET Core 集成测试

集成测试集成测试&#xff0c;也叫组装测试或联合测试。在单元测试的基础上&#xff0c;将所有模块按照设计要求&#xff08;如根据结构图&#xff09;组装成为子系统或系统&#xff0c;进行集成测试。实践表明&#xff0c;一些模块虽然能够单独地工作&#xff0c;但并不能保证…

使用C#开发Android应用之WebApp

近段时间了解了一下VS2017开发安卓应用的一些技术&#xff0c;特地把C#开发WebApp的一些过程记录下来&#xff0c;欢迎大家一起指教、讨论&#xff0c;废话少说&#xff0c;是时候开始表演真正的技术了。。1、新建空白Android应用2、拖一个WebView控件进来3、打开模拟器Genymot…

ASP.NET Core依赖注入深入讨论

这篇文章我们来深入探讨ASP.NET Core、MVC Core中的依赖注入&#xff0c;我们将示范几乎所有可能的操作把依赖项注入到组件中。依赖注入是ASP.NET Core的核心&#xff0c;它能让您应用程序中的组件增强可测试性&#xff0c;还使您的组件只依赖于能够提供所需服务的某些组件。举…

使用静态基类方案让 ASP.NET Core 实现遵循 HATEOAS Restful Web API

Hypermedia As The Engine Of Application State (HATEOAS)HATEOAS&#xff08;Hypermedia as the engine of application state&#xff09;是 REST 架构风格中最复杂的约束&#xff0c;也是构建成熟 REST 服务的核心。它的重要性在于打破了客户端和服务器之间严格的契约&…

【招聘(北京)】.NETCORE开发工程师(微服务方向)

组织&#xff1a;华汽集团北京研发中心位置&#xff1a;北京市朝阳区焦奥中心官网&#xff1a;www.sinoauto.com邮箱&#xff1a;taoxu.weisinoauto.com 项目&#xff1a;打造面向国内汽车后市场用户的一站式云服务平台&#xff08;华汽云&#xff09;&#xff0c;形态包括B2B、…

确保线程安全下使用Queue的Enqueue和Dequeue

场景是这样&#xff0c;假设有一台设备会触发类型为Alarm的告警信号&#xff0c;并把信号添加到一个Queue结构中&#xff0c;每隔一段时间这个Queue会被遍历检查&#xff0c;其中的每个Alarm都会调用一个相应的处理方法。问题在于&#xff0c;检查机制是基于多线程的&#xff0…

编写一个Java程序,其中包含三个线程: 厨师(Chef)、服务员(Waiter)和顾客(Customer)

编写一个Java程序&#xff0c;其中包含三个线程: 厨师(Chef)、服务员(Waiter)和顾客(Customer)。他们的行动如下: 厨师准备菜肴&#xff0c;每次准备一个。服务员等待菜肴准备好&#xff0c;然后将其送到顾客那里。顾客等待服务员送来菜看后才开始吃。所有三个角色应该循环进行…

Hangfire使用ApplicationInsigts监控

起因我司目前使用清真的ApplicationInsights来做程序级监控。&#xff08;ApplicationInsights相关文档: https://azure.microsoft.com/zh-cn/services/application-insights/ &#xff09;其实一切都蛮好的&#xff0c;但是我们基于Hangfire的Job系统却无法被Ai所监控到&#…

NET主流ORM框架分析

接上文我们测试了各个ORM框架的性能&#xff0c;大家可以很直观的看到各个ORM框架与原生的ADO.NET在境删改查的性能差异。这里和大家分享下我对ORM框架的理解及一些使用经验。ORM框架工作原理所有的ORM框架的工作原理都离不开下面这张图&#xff0c;只是每个框架的实现程度不同…

20、java中的类加载机制

1、类加载机制是什么&#xff1f; 类加载机制指的就是jvm将类的信息动态添加到内存并使用的一种机制。 2、那么类加载的具体流程是什么呢&#xff1f; 一般说类加载只有三步&#xff1a;加载、连接和初始化&#xff0c;其中连接包括验证、准备和解析&#xff0c;用于将运行时加…

【北京】BXUG第12期活动基于 .NET Core构建微服务和Xamarin

分享主题&#xff1a;基于 .NET Core构建微服务实战分享分享者&#xff1a;薛锋 北京切尔思科技架构师 兼任东北大学信息安全工程师和技术主播&#xff0c;行业内专注于研究 .NET Core和Web应用&#xff0c;具有比较扎实的技术基础和数年的从业经历。在GitHub上主持数个开…

谈谈ASP.NET Core中的ResponseCaching

前言前面的博客谈的大多数都是针对数据的缓存&#xff0c;今天我们来换换口味。来谈谈在ASP.NET Core中的ResponseCaching&#xff0c;与ResponseCaching关联密切的也就是常说的HTTP缓存。在阅读本文内容之前&#xff0c;默认各位有HTTP缓存相关的基础&#xff0c;主要是Cache-…

使用 dynamic 类型让 ASP.NET Core 实现 HATEOAS 结构的 RESTful API

上一篇写的是使用静态基类方法的实现步骤: 使用dynamic (ExpandoObject)的好处就是可以动态组建返回类型, 之前使用的是ViewModel, 如果想返回结果的话, 肯定需要把ViewModel所有的属性都返回, 如果属性比较多, 就有可能造成性能和灵活性等问题. 而使用ExpandoObject(dynamic)就…

使用 BenchmarkDotnet 测试代码性能

先来点题外话&#xff0c;清明节前把工作辞了&#xff08;去 tm 的垃圾团队&#xff0c;各种拉帮结派、勾心斗角&#xff09;。这次找工作就得慢慢找了&#xff0c;不能急了&#xff0c;希望能找到个好团队&#xff0c;好岗位吧。顺便这段时间也算是比较闲&#xff0c;也能学习…

2017西安交大ACM小学期数论 [阅兵式]

阅兵式 发布时间: 2017年6月25日 12:53 最后更新: 2017年7月3日 09:27 时间限制: 1000ms 内存限制: 128M 描述 阅兵式上&#xff0c;将士们排成一个整齐的方阵&#xff0c;每个将士面朝前方。问正中心的将士能向前看到几个将士&#xff1f;注意&#xff0c;一条直线上的将…

28、jdbc操作数据库(5)

介绍一个稍微封装了jdbc的工具类org.apache.commons.dbutils&#xff0c;使用dbutils可以简化对数据库操作程序的开发。 API介绍 接下来通过实例的方式说一下dbutils的具体使用 添加jar包&#xff1a;commons-dbutils-1.7.jar 增、删、改 进行增、删、改操作&#xff0c;在…

2017西安交大ACM小学期数论 [等差数列]

等差数列 发布时间: 2017年6月25日 13:42 最后更新: 2017年7月3日 09:27 时间限制: 1000ms 内存限制: 128M 描述 给定正整数n&#xff0c;试问存在多少个和为n的等差数列&#xff1f; 当然&#xff0c;等差数列中每一项要为非负整数&#xff0c;且不考虑降序的等差数列。…

上古时期(大雾)的数据结构pdf

分块点分治Treap byWYCby\ WYCby WYC Part1 分块 概念 就是将nnn个数分成若干个块&#xff0c;然后要处理的时候整块一起的加上局部的直接暴力。 如果将块的大小分配好一般每次都是O(n)O(\sqrt n)O(n​)的。 而且因为十分暴力&#xff0c;所以有很多优秀的性质。 实现方法 …

33、JAVA_WEB开发基础之会话机制

会话是什么 一个客户端浏览器与web服务器之间连续发生的一系列请求和响应过程就是会话&#xff0c;这些过程中产生的一系列信息就是会话信息&#xff0c;会话机制就是用于维护这些信息一致性的一种技术。通俗的说就是&#xff0c;一个A账号访问服务器&#xff0c;进行多次交互…