一, 前言
前几篇文章分享了 IdentityServer4 密码模式的基本授权及自定义授权等方式, 最近由于改造一个网关服务, 用到了 IdentityServer4 的授权, 改造过程中发现比较适合基于 Role 角色的授权, 通过不同的角色来限制用户访问不同的 API 资源, 这里我就来分享 IdentityServer4 基于角色的授权详解.
IdentityServer4 历史文章目录
ASP.NET Core IdentityServer4 中的基本概念
ASP.NET Core 中 IdentityServer4 授权中心之应用实战
ASP.NET Core 中 IdentityServer4 授权中心之自定义授权模式
ASP.NET Core 中 IdentityServer4 授权原理及刷新 Token 的应用
ASP.NET Core 中 IdentityServer4 实战之 Claim 详解
没有看过之前的几篇文章, 我建议先回过头看看上面那几篇文章再来看本篇文章, 不过对于大牛来说就可以跳过了....
二, 模拟场景
还是按照我的文章风格套路, 实战之前先来模拟下应用场景, 无场景的实战都是耍流氓, 模拟场景更能让大家投入, 同时也是自我学习, 思考, 总结的结晶之处!!!
对于角色授权大家也不陌生, 大家比较熟悉的应该是 RBAC 的设计, 这里就不阐述 RBAC, 有兴趣的可以百度. 我们这里简单模拟下角色场景
假如有这么一个数据网关服务服务(下面我统称为数据网关), 客户端有三种账号角色(普通用户, 管理员用户, 超级管理员用户), 数据网关针对这三种角色用户分配不同的数据访问权限, 场景图如下:
那么这种场景我们会怎么去设计呢? 这个场景还算比较简单, 角色比较单一, 比较固定, 对于这种场景很多人可能会考虑到通过 Filter 过滤器等方式来实现, 这当然可以. 不过正对这种场景 IdentityServer4 中本身就支持角色授权, 下面我来给大家分享 IdentityServer4 的角色授权.
三, 角色授权实战
授权流程
撸代码之前我们先整理下 IdentityServer4 的 角色授权流程图, 我简单概括画了下, 流程图如下:
场景图概括如下:
客户端分为三种核心角色 (普通用户, 管理员用户, 超级管理 - 老板) 用户, 三种用户访问同一个数据网关(API 资源)
数据网关 (API 资源) 对这三种用户角色做了访问限制.
角色授权流程解释如下:
第一步: 不同的用户携带用户密码等信息访问授权中心 (ids4) 尝试授权
第二步: 授权中心对用户授权通过返回 access_token 给用户同时声明用户的 Role 到 Claim 中..
第三步: 客户端携带拿到的 access_token 尝试请求数据网关(API 资源).
第四步: 数据网关收到客户端的第一次请求会到授权中心请求获得验证公钥.
第五步: 授权中心返回验证公钥给数据网关并且缓存起来, 后面不再到授权中心再次获得验证公钥(只会请求一次, 除非重启服务).
第六步: 数据网关 (ids4) 通过验证网关验证 access_token 是否验证通过, 并且验证请求的客户端用户声明的 Role 是否和请求的 API 资源约定的的角色一致. 如果一致则通过第步返回给用户端, 否则直接拒绝请求.
撸代码
代码继续上面几篇文章的例子的续集, 你懂的, 就不从零开始撸代码啦(强烈建议没看过上面几篇的先看下上面的目录中的几篇, 要不然会一头雾水, 大佬跳过)
要使 IdentityServer4 实现的授权中心支持角色验证的支持, 我们需要在定义的 API 资源中添加角色的引入, 代码如下:
上几篇文章的授权中心 (Jlion.NetCore.Identity.Service) 的
代码如下:
- /// <summary>
- /// 资源
- /// </summary>
- /// <returns></returns>
- public static IEnumerable<ApiResource> GetApiResources()
- {
- return new List<ApiResource>
- {
- new ApiResource(OAuthConfig.UserApi.ApiName,OAuthConfig.UserApi.ApiName),
- };
- }
加入角色的支持代码改造如下:
- /// <summary>
- /// 资源
- /// </summary>
- /// <returns></returns>
- public static IEnumerable<ApiResource> GetApiResources()
- {
- return new List<ApiResource>
- {
- new ApiResource(
- OAuthConfig.UserApi.ApiName,
- OAuthConfig.UserApi.ApiName,
- new List<string>(){JwtClaimTypes.Role }
- ),
- };
- }
API 资源中添加了角色验证的支持后, 需要在用户登录授权成功后声明 Claim 用户的 Role 信息, 代码如下:
改造前代码:
- public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
- {
- public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
- {
- try
- {
- var userName = context.UserName;
- var password = context.Password;
- // 验证用户, 这么可以到数据库里面验证用户名和密码是否正确
- var claimList = await ValidateUserAsync(userName, password);
- // 验证账号
- context.Result = new GrantValidationResult
- (
- subject: userName,
- authenticationMethod: "custom",
- claims: claimList.ToArray()
- );
- }
- catch (Exception ex)
- {
- // 验证异常结果
- context.Result = new GrantValidationResult()
- {
- IsError = true,
- Error = ex.Message
- };
- }
- }
- #region Private Method
- /// <summary>
- /// 验证用户
- /// </summary>
- /// <param name="loginName"></param>
- /// <param name="password"></param>
- /// <returns></returns>
- private async Task<List<Claim>> ValidateUserAsync(string loginName, string password)
- {
- //TODO 这里可以通过用户名和密码到数据库中去验证是否存在,
- // 以及角色相关信息, 我这里还是使用内存中已经存在的用户和密码
- var user = OAuthMemoryData.GetTestUsers();
- if (user == null)
- throw new Exception("登录失败, 用户名和密码不正确");
- return new List<Claim>()
- {
- new Claim(ClaimTypes.Name, $"{loginName}"),
- new Claim(EnumUserClaim.DisplayName.ToString(),"测试用户"),
- new Claim(EnumUserClaim.UserId.ToString(),"10001"),
- new Claim(EnumUserClaim.MerchantId.ToString(),"000100001"),
- };
- }
- #endregion
- }
为了保留之前文章的源代码, 好让之前的文章源代码可追溯, 我这里不在源代码上改造升级, 我直接新增一个用户密码验证器类,
命名为 RoleTestResourceOwnerPasswordValidator, 代码改造如下:
- /// <summary>
- /// 角色授权用户名密码验证器 demo
- /// </summary>
- public class RoleTestResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
- {
- public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
- {
- try
- {
- var userName = context.UserName;
- var password = context.Password;
- // 验证用户, 这么可以到数据库里面验证用户名和密码是否正确
- var claimList = await ValidateUserByRoleAsync(userName, password);
- // 验证账号
- context.Result = new GrantValidationResult
- (
- subject: userName,
- authenticationMethod: "custom",
- claims: claimList.ToArray()
- );
- }
- catch (Exception ex)
- {
- // 验证异常结果
- context.Result = new GrantValidationResult()
- {
- IsError = true,
- Error = ex.Message
- };
- }
- }
- #region Private Method
- /// <summary>
- /// 验证用户(角色 Demo 专用方法)
- /// 这里和之前区分, 主要是为了保留和博客同步源代码
- /// </summary>
- /// <param name="loginName"></param>
- /// <param name="password"></param>
- /// <returns></returns>
- private async Task<List<Claim>> ValidateUserByRoleAsync(string loginName, string password)
- {
- //TODO 这里可以通过用户名和密码到数据库中去验证是否存在,
- // 以及角色相关信息, 我这里还是使用内存中已经存在的用户和密码
- var user = OAuthMemoryData.GetUserByUserName(loginName);
- if (user == null)
- throw new Exception("登录失败, 用户名和密码不正确");
- // 下面的 Claim 声明我为了演示, 硬编码了,
- // 实际生产环境需要通过读取数据库的信息并且来声明
- return new List<Claim>()
- {
- new Claim(ClaimTypes.Name, $"{user.UserName}"),
- new Claim(EnumUserClaim.DisplayName.ToString(),user.DisplayName),
- new Claim(EnumUserClaim.UserId.ToString(),user.UserId.ToString()),
- new Claim(EnumUserClaim.MerchantId.ToString(),user.MerchantId.ToString()),
- new Claim(JwtClaimTypes.Role.ToString(),user.Role.ToString())
- };
- }
- #endregion
- }
为了方便演示, 我直接把 Role 定义成了一个公共枚举 EnumUserRole, 代码如下:
- /// <summary>
- /// 角色枚举
- /// </summary>
- public enum EnumUserRole
- {
- Normal,
- Manage,
- SupperManage
- }
GetUserByUserName 中硬编码创建了三个角色的用户, 代码如下:
- /// <summary>
- /// 为了演示, 硬编码了,
- /// 这个方法可以通过 DDD 设计到底层数据库去查询数据库
- /// </summary>
- /// <param name="userName"></param>
- /// <returns></returns>
- public static UserModel GetUserByUserName(string userName)
- {
- var normalUser = new UserModel()
- {
- DisplayName = "张三",
- MerchantId = 10001,
- Password = "123456",
- Role = Enums.EnumUserRole.Normal,
- SubjectId = "1",
- UserId = 20001,
- UserName = "testNormal"
- };
- var manageUser = new UserModel()
- {
- DisplayName = "李四",
- MerchantId = 10001,
- Password = "123456",
- Role = Enums.EnumUserRole.Manage,
- SubjectId = "1",
- UserId = 20001,
- UserName = "testManage"
- };
- var supperManageUser = new UserModel()
- {
- DisplayName = "dotNET 博士",
- MerchantId = 10001,
- Password = "123456",
- Role = Enums.EnumUserRole.SupperManage,
- SubjectId = "1",
- UserId = 20001,
- UserName = "testSupperManage"
- };
- var list = new List<UserModel>() {
- normalUser,
- manageUser,
- supperManageUser
- };
- return list?.Where(item => item.UserName.Equals(userName))?.FirstOrDefault();
- }
好了, 现在用户授权通过后声明的 Role 也已经完成了, 我上面使用的是 JwtClaimTypes 默认支持的 Role, 你也可以不使用 JwtClaimTypes 类, 可以自定义类来实现.
最后为了让新关注我的博客用户没看过之前几篇文章的用户不至于一头雾水, 我把注册 ids 中间件代码还是贴出来,
注册新的用户名密码验证器到 DI 中 代码如下:
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddControllers();
- #region 数据库存储方式
- services.AddIdentityServer()
- .AddDeveloperSigningCredential()
- .AddInMemoryApiResources(OAuthMemoryData.GetApiResources())
- //.AddInMemoryClients(OAuthMemoryData.GetClients())
- .AddClientStore<ClientStore>()
- //.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
- .AddResourceOwnerValidator<RoleTestResourceOwnerPasswordValidator>()
- .AddExtensionGrantValidator<WeiXinOpenGrantValidator>()
- .AddProfileService<UserProfileService>();// 添加微信端自定义方式的验证
- #endregion
- }
- public void Configure(IApplicationBuilder App, IwebHostEnvironment env)
- {
- if (env.IsDevelopment())
- {
- App.UseDeveloperExceptionPage();
- }
- // 使用 IdentityServer4 的中间件
- App.UseIdentityServer();
- App.UseRouting();
- App.UseAuthorization();
- App.UseEndpoints(endpoints =>
- {
- endpoints.MapControllers();
- });
- }
授权中心的角色支持代码撸完了, 我们来改造上几篇文章中说到的用户网关服务, 这里我就叫数据网关,
项目: Jlion.NetCore.Identity.UserApiService
上一篇关于 ASP.NET Core 中 IdentityServer4 实战之 Claim 详解
文章中在数据网关服务中新增了 UserController 控制器, 并添加了一个访问用户基本的 Claim 信息接口, 之前的代码如下:
- [ApiController]
- [Route("[controller]")]
- public class UserController : ControllerBase
- {
- private readonly ILogger<UserController> _logger;
- public UserController(ILogger<UserController> logger)
- {
- _logger = logger;
- }
- [Authorize]
- [HttpGet]
- public async Task<object> Get()
- {
- var userId = User.UserId();
- return new
- {
- name = User.Name(),
- userId = userId,
- displayName = User.DisplayName(),
- merchantId = User.MerchantId(),
- };
- }
- }
上面的代码中 Authorize 没有指定 Role, 那相当于所有的用户都可以访问这个接口, 接下来, 我们在 UserController 中创建一个只能是超级管理员角色才能访问的接口, 代码如下
- [Authorize(Roles =nameof(EnumUserRole.SupperManage))]
- [HttpGet("{id}")]
- public async Task<object> Get(int id)
- {
- var userId = User.UserId();
- return new
- {
- name = User.Name(),
- userId = userId,
- displayName = User.DisplayName(),
- merchantId = User.MerchantId(),
- roleName=User.Role()// 获得当前登录用户的角色
- };
- }
到这里数据网关代码也已经改造完了, 我们接下来就是运行结果看看是否正确.
运行
我们分别通过命令行运行我们的授权网关服务和数据网关服务, 分别如下图:
授权网关还是指定 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 博士" 关注, 或者微信扫下面的二维码进行关注, 同时大家也可以积极的分享或点个右下角的推荐, 让更多人的关注到我的文章.
来源: https://www.cnblogs.com/jlion/p/12571620.html