Asp.Net Core 中IdentityServer4 实战之角色授权详解

一、前言

前几篇文章分享了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)的
代码如下:

 /// <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博士"关注,或者微信扫下面的二维码进行关注,同时大家也可以积极的分享或点个右下角的推荐,让更多人的关注到我的文章。

posted @ 2020-03-30 07:25  Jlion  阅读(5012)  评论(1编辑  收藏  举报