一、前言

前几篇文章分享了IdentityServer4密码模式的基本授权及自定义授权等方式,最近由于改造一个网关服务,用到了IdentityServer4的授权,改造过程中发现比较适合基于Role角色的授权,通过不同的角色来限制用户访问不同的Api资源,这里我就来分享IdentityServer4基于角色的授权详解。

IdentityServer4 历史文章目录

没有看过之前的几篇文章,我建议先回过头看看上面那几篇文章再来看本篇文章,不过对于大牛来说就可以跳过了。。。。

二、模拟场景

还是按照我的文章风格套路,实战之前先来模拟下应用场景,无场景的实战都是耍流氓,模拟场景更能让大家投入,同时也是自我学习、思考、总结的结晶之处!!!

对于角色授权大家也不陌生,大家比较熟悉的应该是RBAC的设计,这里就不阐述RBAC,有兴趣的可以百度。我们这里简单模拟下角色场景

假如有这么一个数据网关服务服务(下面我统称为数据网关),客户端有三种账号角色(普通用户、管理员用户、超级管理员用户),数据网关针对这三种角色用户分配不同的数据访问权限,场景图如下:

那么这种场景我们会怎么去设计呢?这个场景还算比较简单,角色比较单一,比较固定,对于这种场景很多人可能会考虑到通过Filter过滤器等方式来实现,这当然可以。不过正对这种场景IdentityServer4中本身就支持角色授权,下面我来给大家分享IdentityServer4的角色授权.

三、角色授权实战

授权流程

撸代码之前我们先整理下IdentityServer4的 角色授权流程图,我简单概括画了下,流程图如下:

场景图概括如下:

  • 客户端分为三种核心角色(普通用户、管理员用户、超级管理-老板)用户,三种用户访问同一个数据网关(API资源)
  • 数据网关(API资源)对这三种用户角色做了访问限制。

角色授权流程解释如下:

  • 第一步: 不同的用户携带用户密码等信息访问授权中心(ids4)尝试授权
  • 第二步: 授权中心对用户授权通过返回access_token给用户同时声明用户的RoleClaim中。。
  • 第三步: 客户端携带拿到的access_token尝试请求数据网关(API资源)。
  • 第四步:数据网关收到客户端的第一次请求会到授权中心请求获得验证公钥。
  • 第五步:授权中心返回验证公钥数据网关并且缓存起来,后面不再到授权中心再次获得验证公钥(只会请求一次,除非重启服务)。
  • 第六步:数据网关(ids4)通过验证网关验证access_token是否验证通过,并且验证请求的客户端用户声明的Role是否和请求的API资源约定的的角色一致。如果一致则通过第步返回给用户端,否则直接拒绝请求.

撸代码

代码继续上面几篇文章的例子的续集,你懂的,就不从零开始撸代码啦(强烈建议没看过上面几篇的先看下上面的目录中的几篇,要不然会一头雾水,大佬跳过)

要使IdentityServer4实现的授权中心支持角色验证的支持,我们需要在定义的API资源中添加角色的引入,代码如下:

上几篇文章的授权中心(Jlion.NetCore.Identity.Service)的

代码如下:

  1. /// <summary>
  2. /// 资源
  3. /// </summary>
  4. /// <returns></returns>
  5. public static IEnumerable<ApiResource> GetApiResources()
  6. {
  7. return new List<ApiResource>
  8. {
  9. new ApiResource(OAuthConfig.UserApi.ApiName,OAuthConfig.UserApi.ApiName),
  10. };
  11. }

加入角色的支持代码改造如下:

  1. /// <summary>
  2. /// 资源
  3. /// </summary>
  4. /// <returns></returns>
  5. public static IEnumerable<ApiResource> GetApiResources()
  6. {
  7. return new List<ApiResource>
  8. {
  9. new ApiResource(
  10. OAuthConfig.UserApi.ApiName,
  11. OAuthConfig.UserApi.ApiName,
  12. new List<string>(){JwtClaimTypes.Role }
  13. ),
  14. };
  15. }

API资源中添加了角色验证的支持后,需要在用户登录授权成功后声明Claim用户的Role信息,代码如下:

改造前代码:

  1. public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
  2. {
  3. public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
  4. {
  5. try
  6. {
  7. var userName = context.UserName;
  8. var password = context.Password;
  9. //验证用户,这么可以到数据库里面验证用户名和密码是否正确
  10. var claimList = await ValidateUserAsync(userName, password);
  11. // 验证账号
  12. context.Result = new GrantValidationResult
  13. (
  14. subject: userName,
  15. authenticationMethod: "custom",
  16. claims: claimList.ToArray()
  17. );
  18. }
  19. catch (Exception ex)
  20. {
  21. //验证异常结果
  22. context.Result = new GrantValidationResult()
  23. {
  24. IsError = true,
  25. Error = ex.Message
  26. };
  27. }
  28. }
  29. #region Private Method
  30. /// <summary>
  31. /// 验证用户
  32. /// </summary>
  33. /// <param name="loginName"></param>
  34. /// <param name="password"></param>
  35. /// <returns></returns>
  36. private async Task<List<Claim>> ValidateUserAsync(string loginName, string password)
  37. {
  38. //TODO 这里可以通过用户名和密码到数据库中去验证是否存在,
  39. // 以及角色相关信息,我这里还是使用内存中已经存在的用户和密码
  40. var user = OAuthMemoryData.GetTestUsers();
  41. if (user == null)
  42. throw new Exception("登录失败,用户名和密码不正确");
  43. return new List<Claim>()
  44. {
  45. new Claim(ClaimTypes.Name, $"{loginName}"),
  46. new Claim(EnumUserClaim.DisplayName.ToString(),"测试用户"),
  47. new Claim(EnumUserClaim.UserId.ToString(),"10001"),
  48. new Claim(EnumUserClaim.MerchantId.ToString(),"000100001"),
  49. };
  50. }
  51. #endregion
  52. }

为了保留之前文章的源代码,好让之前的文章源代码可追溯,我这里不在源代码上改造升级,我直接新增一个用户密码验证器类,

命名为RoleTestResourceOwnerPasswordValidator,代码改造如下:

  1. /// <summary>
  2. /// 角色授权用户名密码验证器demo
  3. /// </summary>
  4. public class RoleTestResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
  5. {
  6. public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
  7. {
  8. try
  9. {
  10. var userName = context.UserName;
  11. var password = context.Password;
  12. //验证用户,这么可以到数据库里面验证用户名和密码是否正确
  13. var claimList = await ValidateUserByRoleAsync(userName, password);
  14. // 验证账号
  15. context.Result = new GrantValidationResult
  16. (
  17. subject: userName,
  18. authenticationMethod: "custom",
  19. claims: claimList.ToArray()
  20. );
  21. }
  22. catch (Exception ex)
  23. {
  24. //验证异常结果
  25. context.Result = new GrantValidationResult()
  26. {
  27. IsError = true,
  28. Error = ex.Message
  29. };
  30. }
  31. }
  32. #region Private Method
  33. /// <summary>
  34. /// 验证用户(角色Demo 专用方法)
  35. /// 这里和之前区分,主要是为了保留和博客同步源代码
  36. /// </summary>
  37. /// <param name="loginName"></param>
  38. /// <param name="password"></param>
  39. /// <returns></returns>
  40. private async Task<List<Claim>> ValidateUserByRoleAsync(string loginName, string password)
  41. {
  42. //TODO 这里可以通过用户名和密码到数据库中去验证是否存在,
  43. // 以及角色相关信息,我这里还是使用内存中已经存在的用户和密码
  44. var user = OAuthMemoryData.GetUserByUserName(loginName);
  45. if (user == null)
  46. throw new Exception("登录失败,用户名和密码不正确");
  47. //下面的Claim 声明我为了演示,硬编码了,
  48. //实际生产环境需要通过读取数据库的信息并且来声明
  49. return new List<Claim>()
  50. {
  51. new Claim(ClaimTypes.Name, $"{user.UserName}"),
  52. new Claim(EnumUserClaim.DisplayName.ToString(),user.DisplayName),
  53. new Claim(EnumUserClaim.UserId.ToString(),user.UserId.ToString()),
  54. new Claim(EnumUserClaim.MerchantId.ToString(),user.MerchantId.ToString()),
  55. new Claim(JwtClaimTypes.Role.ToString(),user.Role.ToString())
  56. };
  57. }
  58. #endregion
  59. }

为了方便演示,我直接把Role定义成了一个公共枚举EnumUserRole,代码如下:

  1. /// <summary>
  2. /// 角色枚举
  3. /// </summary>
  4. public enum EnumUserRole
  5. {
  6. Normal,
  7. Manage,
  8. SupperManage
  9. }

GetUserByUserName中硬编码创建了三个角色的用户,代码如下:

  1. /// <summary>
  2. /// 为了演示,硬编码了,
  3. /// 这个方法可以通过DDD设计到底层数据库去查询数据库
  4. /// </summary>
  5. /// <param name="userName"></param>
  6. /// <returns></returns>
  7. public static UserModel GetUserByUserName(string userName)
  8. {
  9. var normalUser = new UserModel()
  10. {
  11. DisplayName = "张三",
  12. MerchantId = 10001,
  13. Password = "123456",
  14. Role = Enums.EnumUserRole.Normal,
  15. SubjectId = "1",
  16. UserId = 20001,
  17. UserName = "testNormal"
  18. };
  19. var manageUser = new UserModel()
  20. {
  21. DisplayName = "李四",
  22. MerchantId = 10001,
  23. Password = "123456",
  24. Role = Enums.EnumUserRole.Manage,
  25. SubjectId = "1",
  26. UserId = 20001,
  27. UserName = "testManage"
  28. };
  29. var supperManageUser = new UserModel()
  30. {
  31. DisplayName = "dotNET博士",
  32. MerchantId = 10001,
  33. Password = "123456",
  34. Role = Enums.EnumUserRole.SupperManage,
  35. SubjectId = "1",
  36. UserId = 20001,
  37. UserName = "testSupperManage"
  38. };
  39. var list = new List<UserModel>() {
  40. normalUser,
  41. manageUser,
  42. supperManageUser
  43. };
  44. return list?.Where(item => item.UserName.Equals(userName))?.FirstOrDefault();
  45. }

好了,现在用户授权通过后声明的Role也已经完成了,我上面使用的是JwtClaimTypes 默认支持的Role,你也可以不使用JwtClaimTypes类,可以自定义类来实现。

最后为了让新关注我的博客用户没看过之前几篇文章的用户不至于一头雾水,我把注册ids中间件代码还是贴出来,

注册新的用户名密码验证器到DI中 代码如下:

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddControllers();
  4. #region 数据库存储方式
  5. services.AddIdentityServer()
  6. .AddDeveloperSigningCredential()
  7. .AddInMemoryApiResources(OAuthMemoryData.GetApiResources())
  8. //.AddInMemoryClients(OAuthMemoryData.GetClients())
  9. .AddClientStore<ClientStore>()
  10. //.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
  11. .AddResourceOwnerValidator<RoleTestResourceOwnerPasswordValidator>()
  12. .AddExtensionGrantValidator<WeiXinOpenGrantValidator>()
  13. .AddProfileService<UserProfileService>();//添加微信端自定义方式的验证
  14. #endregion
  15. }
  16. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  17. {
  18. if (env.IsDevelopment())
  19. {
  20. app.UseDeveloperExceptionPage();
  21. }
  22. //使用IdentityServer4 的中间件
  23. app.UseIdentityServer();
  24. app.UseRouting();
  25. app.UseAuthorization();
  26. app.UseEndpoints(endpoints =>
  27. {
  28. endpoints.MapControllers();
  29. });
  30. }

授权中心的角色支持代码撸完了,我们来改造上几篇文章中说到的用户网关服务,这里我就叫数据网关

项目:Jlion.NetCore.Identity.UserApiService

上一篇关于Asp.Net Core 中IdentityServer4 实战之 Claim详解

文章中在数据网关服务中新增了UserController控制器,并添加了一个访问用户基本的Claim信息接口,之前的代码如下:

  1. [ApiController]
  2. [Route("[controller]")]
  3. public class UserController : ControllerBase
  4. {
  5. private readonly ILogger<UserController> _logger;
  6. public UserController(ILogger<UserController> logger)
  7. {
  8. _logger = logger;
  9. }
  10. [Authorize]
  11. [HttpGet]
  12. public async Task<object> Get()
  13. {
  14. var userId = User.UserId();
  15. return new
  16. {
  17. name = User.Name(),
  18. userId = userId,
  19. displayName = User.DisplayName(),
  20. merchantId = User.MerchantId(),
  21. };
  22. }
  23. }

上面的代码中Authorize没有指定Role,那相当于所有的用户都可以访问这个接口,接下来,我们在UserController中创建一个只能是超级管理员角色才能访问的接口,代码如下

  1. [Authorize(Roles =nameof(EnumUserRole.SupperManage))]
  2. [HttpGet("{id}")]
  3. public async Task<object> Get(int id)
  4. {
  5. var userId = User.UserId();
  6. return new
  7. {
  8. name = User.Name(),
  9. userId = userId,
  10. displayName = User.DisplayName(),
  11. merchantId = User.MerchantId(),
  12. roleName=User.Role()//获得当前登录用户的角色
  13. };
  14. }

到这里数据网关代码也已经改造完了,我们接下来就是运行结果看看是否正确。

运行

我们分别通过命令行运行我们的授权网关服务和数据网关服务,分别如下图:

授权网关还是指定5000 端口,如下图:



数据网关跟之前几篇文章一样指定 5001 端口,如下图:

现在授权网关数据网关都已经完美运行起来了,接下来我们通过postman模拟请求。

先来通过普通用户(testNormal)请求授权中心获得access_token,如下图:



请求验证通过,

再来通过获取到的access_token 获取普通接口:



也完美获取到数据

再来访问下标注了supperManage超级管理员的角色接口,如下图:



结果跟预想的一样,返回了403访问被拒绝,其他账号运行也是一样,我这里就不一一去运行访问测试了,有兴趣的同学可以到github 上拉起我的源代码进行运行测试,

到这里基于ids4角色授权基础应用也完成了。

结束语:上面分享学习了IdentityServer4 进行角色授权的实战例子,但是从上面的例子中有一个不好的弊端,就是每个api访问都需要硬编码进行指定Role 这在生产环境中很不现实和灵活,Role角色这个东西都是通过后台自管理,进行灵活配置角色和资源的,那IdentityServer4 有没有什么好的方式实现呢?留给大家思考,思考就有学习的目标,也是思维的进步。

博客系列源代码地址:https://github.com/a312586670/NetCoreDemo

感谢语:三月份即将过去,三月份同时也是美好的开始,我的博客从三月份开始整理分享,传承着以一起学习,共同进步为目标,自我自律,开始分享相关技术。文章持续性同步至我的微信公众号【dotNET博士】,这个月来初见成效,一个月内已经荣获500+以上的粉丝,也感谢大家一直以来对我的关注,你的关注让我更有动力分享更好的原创技术文章。还没有关注微信公众号的,搜索"dotNET博士"关注,或者微信扫下面的二维码进行关注,同时大家也可以积极的分享或点个右下角的推荐,让更多人的关注到我的文章。

Asp.Net Core 中IdentityServer4 实战之角色授权详解的更多相关文章

  1. Asp.Net Core 中IdentityServer4 实战之 Claim详解

    一.前言 由于疫情原因,让我开始了以博客的方式来学习和分享技术(持续分享的过程也是自己学习成长的过程),同时也让更多的初学者学习到相关知识,如果我的文章中有分析不到位的地方,还请大家多多指教:以后我会 ...

  2. Asp.Net Core 中IdentityServer4 授权中心之应用实战

    一.前言 查阅了大多数相关资料,查阅到的IdentityServer4 的相关文章大多是比较简单并且多是翻译官网的文档编写的,我这里在 Asp.Net Core 中IdentityServer4 的应 ...

  3. Asp.Net Core 中IdentityServer4 授权中心之自定义授权模式

    一.前言 上一篇我分享了一篇关于 Asp.Net Core 中IdentityServer4 授权中心之应用实战 的文章,其中有不少博友给我提了问题,其中有一个博友问我的一个场景,我给他解答的还不够完 ...

  4. Asp.Net Core 中IdentityServer4 授权原理及刷新Token的应用

    一.前言 上面分享了IdentityServer4 两篇系列文章,核心主题主要是密码授权模式及自定义授权模式,但是仅仅是分享了这两种模式的使用,这篇文章进一步来分享IdentityServer4的授权 ...

  5. 从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权

    OpenIDConnect是一个身份验证服务,而Oauth2.0是一个授权框架,在前面几篇文章里通过IdentityServer4实现了基于Oauth2.0的客户端证书(Client_Credenti ...

  6. IdentityServer4实战 - JWT Token Issuer 详解

    原文:IdentityServer4实战 - JWT Token Issuer 详解 一.前言 本文为系列补坑之作,拖了许久决定先把坑填完. 下文演示所用代码采用的 IdentityServer4 版 ...

  7. ASP.NET Core WebApi基于JWT实现接口授权验证

    一.ASP.Net Core WebApi JWT课程前言 我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再 ...

  8. 【新书推荐】《ASP.NET Core微服务实战:在云环境中开发、测试和部署跨平台服务》 带你走近微服务开发

    <ASP.NET Core 微服务实战>译者序:https://blog.jijiechen.com/post/aspnetcore-microservices-preface-by-tr ...

  9. ASP.NET Core分布式项目实战

    ASP.NET Core开发者成长路线图 asp.net core 官方文档 https://docs.microsoft.com/zh-cn/aspnet/core/getting-started/ ...

随机推荐

  1. linux增加history时间戳

    增加环境变量到/etc/profile export HISTTIMEFORMAT="%Y-%m-%d %H:%M:%S " export HISTSIZE=9999

  2. iOS常用框架源码分析

    SDWebImage NSCache 类似可变字典,线程安全,使用可变字典自定义实现缓存时需要考虑加锁和释放锁 在内存不足时NSCache会自动释放存储的对象,不需要手动干预 NSCache的key不 ...

  3. 提权篇之简单介绍和exp利用过程

    正文开始.... 提权的方法有很多种,因为一开始我入门的时候是看的小迪的网络教程,当然也推荐大家去看小迪的教程,或者直接小迪的实地培训班.这个可没什么利益关系,我认识他,他可不认识我,,但是我是在网上 ...

  4. Java 读取Word中的脚注、尾注

    本文介绍读取Word中的脚注及尾注的方法,添加脚注.尾注可以参考这篇文章. 注:本文使用了Word类库(Free Spire.Doc for Java 免费版)来读取,获取该类库可通过官网下载,并解压 ...

  5. 微服务架构-Gradle下载安装配置教程

    一.开发条件 JDK8下载地址:https://www.oracle.com/java/technologies/javase-jdk8-downloads.html Eclipse下载地址:http ...

  6. 029.核心组件-Controller Manager

    一 Controller Manager原理 1.1 Controller Manager概述 一般来说,智能系统和自动系统通常会通过一个"控制系统"来不断修正系统的工作状态.在K ...

  7. echart图表中legend不显示问题

    主要是legend中的name要和series中的name对应上.

  8. ajax3

    json json:JavaScript对象表示方法(JavaScript object notation) json:是存储和交换文本信息的语法,类似与xml.他使用键值对的方式来组织,易于人们阅读 ...

  9. (转)浅析epoll – epoll例子以及分析

    原文地址:http://www.cppfans.org/1419.html 浅析epoll – epoll例子以及分析 上篇我们讲到epoll的函数和性能.这一篇用用这些个函数,给出一个最简单的epo ...

  10. 使用EPX Studio 7.0 下载网站验证码

    implementation var Document_: DispHTMLDocument; //用于处理网页文档对象 EPX: IExcelPanelXDisp; procedure TForm1 ...