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

原文地址:https://www.cnblogs.com/jlion/p/12571620.html

时间: 2024-10-13 07:59:28

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

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

一.前言 由于疫情原因,让我开始了以博客的方式来学习和分享技术(持续分享的过程也是自己学习成长的过程),同时也让更多的初学者学习到相关知识,如果我的文章中有分析不到位的地方,还请大家多多指教:以后我会持续更新我的文章,望大家多多支持和关注. 上几篇文章主要分享了IdentityServer4在Asp.Net Core 3.x 中的应用,在上面的几篇分享中有一部分博友问了我这么一个问题"他通过IdentityServer4 来搭建授权中心网关服务,怎么才能在访问受保护的Api资源中获取到用户的相关

【ASP.NET Core分布式项目实战】(一)IdentityServer4登录中心、oauth密码模式identity server4实现

原文:[ASP.NET Core分布式项目实战](一)IdentityServer4登录中心.oauth密码模式identity server4实现 本博客根据http://video.jessetalk.cn/my/course/5视频整理 资料 OAuth2 流程:http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html 博客园晓晨的关于identityServer4的中文文档地址: http://www.cnblogs.com/stulzq

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

<ASP.NET Core 微服务实战>译者序:https://blog.jijiechen.com/post/aspnetcore-microservices-preface-by-translator/ "微服务"的概念在 2014 年正式提出之后,越来越多的团队开始用它来设计自己的业务系统,各种微服务框架和开发过程管理方法也同时兴起.不断成熟.微服务设计方法清晰地定义了各个开发团队的业务边界,微服务框架以不同的方式实现了服务之间的协作与集成,根据康威定律我们可以推导这

【ASP.NET Core分布式项目实战】(二)oauth2 + oidc 实现 server部分

原文:[ASP.NET Core分布式项目实战](二)oauth2 + oidc 实现 server部分 本博客根据http://video.jessetalk.cn/my/course/5视频整理(内容可能会有部分,推荐看源视频学习) 资料 我们基于之前的MvcCookieAuthSample来做开发 MvcCookieAuthSample下载地址:https://files.cnblogs.com/files/wyt007/ASPNETCore%E5%BF%AB%E9%80%9F%E5%85

ASP.NET Core 中的依赖注入 [共7篇]

一.控制反转(IoC) ASP.NET Core在启动以及后续针对每个请求的处理过程中的各个环节都需要相应的组件提供相应的服务,为了方便对这些组件进行定制,ASP.NET通过定义接口的方式对它们进行了“标准化”,我们将这些标准化的组件称为服务,ASP.NET在内部专门维护了一个DI容器来提供所需的服务.要了解这个DI容器以及现实其中的服务提供机制,我们先得知道什么是DI(Dependence Injection),而一旦我们提到DI,又不得不说IoC(Inverse of Control)… [

【翻译】asp.net core中使用MediatR

这篇文章来自:https://ardalis.com/using-mediatr-in-aspnet-core-apps 本文作为翻译,有一些单词翻译成中文可能会有一些误解(对于读者)或者错误(对于作者)的地方,所以在文章中你可以看到一些单词没有进行翻译.如果有不对的地方,请指出,谢谢. 在Asp.net core中使用MediatR 我已经开始考虑使用MediatR作为我的领域事件实现.为了达到这个目的,我用asp.net core创建了一个示例程序,总的来说是非常容易的,如果你之前没有使用过

ASP.NET Core 中的 WebSocket 支持(转自MSDN)

本文介绍 ASP.NET Core 中 WebSocket 的入门方法. WebSocket (RFC 6455) 是一个协议,支持通过 TCP 连接建立持久的双向信道. 它用于从快速实时通信中获益的应用,如聊天.仪表板和游戏应用. 如果不明白什么是WebSocket可以参考这篇文章 系统必备 ASP.NET Core 1.1 或更高版本 支持 ASP.NET Core 的任何操作系统: Windows 7/Windows Server 2008 或更高版本 Linux macOS 如果应用在安

asp.net core 使用identityServer4的密码模式来进行身份认证(一)

原文:asp.net core 使用identityServer4的密码模式来进行身份认证(一) IdentityServer4是ASP.NET Core的一个包含OpenID和OAuth 2.0协议的框架.具体Oauth 2.0和openId请百度. 前言本博文适用于前后端分离或者为移动产品来后端api的身份认证功能. 一 首先第一步使用Nuge包管理器下载IdentityServer4的包. 第二部,添加一个名叫Config的类. 这个类的作用是对一些api和client进行配置的. pub

asp.net core 使用identityServer4的密码模式来进行身份认证(2) 认证授权原理

原文:asp.net core 使用identityServer4的密码模式来进行身份认证(2) 认证授权原理 前言:本文将会结合asp.net core 认证源码来分析起认证的原理与流程.asp.net core版本2.2 对于大部分使用asp.net core开发的人来说. 下面这几行代码应该很熟悉了. services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { o