前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Asp.net core IdentityServer4与传统基于角色的权限系统的集成

Asp.net core IdentityServer4与传统基于角色的权限系统的集成

作者头像
乔达摩@嘿
发布2022-05-10 08:39:25
9610
发布2022-05-10 08:39:25
举报
文章被收录于专栏:嘿dotNet

写在前面

因为最近在忙别的,好久没水文了 今天来水一篇;

在学习或者做权限系统技术选型的过程中,经常有朋友有这样的疑问 :

“IdentityServer4的能不能做到与传统基于角色的权限系统集成呢?”

“我的公司有几百个接口,IdentityServer4能不能做到关联用户,给这些用户授予不同的接口的权限呢?”

我的回答是:是的,可以!

同时,我还想补充下,IdentityServer4是给我们的授权流程/需求提供一个新的 标准化的选择,而不是限制你的需求;它是一个基础的框架,你可以根据你的需求自定义成任意你要的样子。

OK,下面开始说说我的实现思路,不一定最优只为抛砖引玉。

开始之前

先准备好两个WebApi 项目,分别有两个接口

Hei.UserApi:6001

GetUsername: https://localhost:6001/api/profile/getusername

GetScore: https://localhost:6001/api/Credit/GetScore //用户信用分要求高,期望管理员才可以调用

Hei.OrderApi:6002

GetOrderNo:https://localhost:6002/api/Order/GetOrderNo

GetAddress: https://localhost:6002/api/Delivery/GetAddress //用户地址敏感,期望管理员才可以调用

实现请看源码

准备好两个角色:

R01 管理员

R02 普通用户

准备好两个用户

Bob: subid=1001,普通用户

Alice: subid=1002,管理员

实际用户有多个角色的,本文为了简化问题,一个用户只允许一种角色

角色对应的权限

管理员:可以调用 Hei.UserApiHei.OrderApi的所有接口;

普通用户:只可以调用 Hei.UserApi->GetUsername,和Hei.OrderApi->GetOrderNo;

实现思路

先来看晓晨大佬画的 access_token 验证交互过程图

可以看到,Token在首次被服务端验证后,后续的验证都在客户端验证的,本文的重点就在这里,需要判断token有没有权限,重写这部分即可;

开始实现

服务端

1、生成自定义token

1、 IdentityServer4 服务端重写IResourceOwnerPasswordValidatorIProfileService 两个接口生成携带有自定义信息的access_token

代码语言:javascript
复制
public class CustomResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
    public CustomResourceOwnerPasswordValidator()
    {
    }

    public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
    {
        if (!string.IsNullOrEmpty(context.UserName) && !string.IsNullOrEmpty(context.Password))
        {
            var loginUser = UserService.Users.First(c => c.Username == context.UserName && c.Password == context.Password);

            if (loginUser != null)
            {
                context.Result = new GrantValidationResult(loginUser.SubjectId, OidcConstants.AuthenticationMethods.Password, new Claim[]{new Claim("my_phone","10086")}); //这里增加自定义信息
                return Task.CompletedTask;
            }
        }
        return Task.CompletedTask;
    }
}

StartUp.cs 启用

代码语言:javascript
复制
builder.AddResourceOwnerValidator<CustomResourceOwnerPasswordValidator>();
builder.AddProfileService<CustomProfileService>();

2、请求一个token来看看:

可以看到我这里token携带有了自定义信息 my_phone,同样的,你可以把角色id直接放这里,或者直接跟用户的subid关联(本demo就是);

客户端

1、自定义授权标签CustomRBACAuthorize

代码语言:javascript
复制
    public class CustomRBACAuthorizeAttribute : AuthorizeAttribute
    {
        public CustomRBACAuthorizeAttribute(string policyName="")
        {
            this.PolicyName = policyName;
        }

        public string PolicyName
        {
            get
            {
                return PolicyName;
            }
            set
            {
                Policy = $"{Const.PolicyCombineIdentityServer4ExternalRBAC}{value.ToString()}";
            }
        }
    }

后面接口打这个标签就表示使用基于自定义的与权限校验

2、自定义授权 IAuthorizationRequirement

代码语言:javascript
复制
   public class CustomRBACRequirement: IAuthorizationRequirement
    {
        public string PolicyName { get; }

        public CustomRBACRequirement(string policyName)
        {
            this.PolicyName = policyName;
        }
    }

3、自定义IAuthorizationPolicyProvider

代码语言:javascript
复制
public class CustomRBACPolicyProvider : IAuthorizationPolicyProvider
    {
        private readonly IConfiguration _configuration;
        public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }

        public CustomRBACPolicyProvider(IConfiguration configuration, IOptions<AuthorizationOptions> options)
        {
            _configuration = configuration;
            FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
        }


        public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
        {
            return FallbackPolicyProvider.GetDefaultPolicyAsync();
        }

        public Task<AuthorizationPolicy> GetFallbackPolicyAsync()
        {
            return Task.FromResult<AuthorizationPolicy>(null);
        }

        public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
        {
            if (policyName.StartsWith(Const.PolicyCombineIdentityServer4ExternalRBAC, StringComparison.OrdinalIgnoreCase))
            {
                var policys = new AuthorizationPolicyBuilder();
                //这里使用自定义Requirement
                policys.AddRequirements(new CustomRBACRequirement(policyName.Replace(Const.PolicyCombineIdentityServer4ExternalRBAC,"")));
                return Task.FromResult(policys.Build());
            }

            return Task.FromResult<AuthorizationPolicy>(null);

        }
      }  

4、自定义Requirement的的 AuthorizationHandler

代码语言:javascript
复制
/// <summary>
/// 处理CustomRBACRequirement的逻辑
/// </summary>
/// <param name="context"></param>
/// <param name="requirement"></param>
/// <returns></returns>
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomRBACRequirement requirement)
{
    var subid = context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    var routeData = _httpContextAccessor.HttpContext?.GetRouteData();

    var curentAction = routeData?.Values["action"]?.ToString();
    var curentController = routeData?.Values["controller"]?.ToString();

    //入口程序集,用来标识某个api
    var apiName = Assembly.GetEntryAssembly().GetName().Name;

    if (string.IsNullOrWhiteSpace(subid) == false && string.IsNullOrWhiteSpace(curentAction) == false && string.IsNullOrWhiteSpace(curentController) == false)
    {
        //核心就在这里了,查出用户subid对应的角色权限,然后做处理判断有没有当前接口的权限
        //我这里是demo就简单的模拟下,真实的权限数据应该都是写数据库或接口的
        var userPermission = PermissionService.GetUserPermissionBySubid(apiName, subid);
        if (userPermission != null && userPermission.Authorised.ContainsKey(curentController))
        {                    
            var authActions = userPermission.Authorised[curentController];
                    
            //这里判断当前用户的角色有当前action/controllers的权限
            //(真实的权限划分由你自己定义,比如你划分了只读接口,只写接口、特殊权限接口、内部接口等,在管理后台上分组,打标签/标记然后授予角色就行)
            if (authActions?.Any(action => action == curentAction) == true)
            {
                context.Succeed(requirement);
            }
        }
    }

    return Task.CompletedTask;
}

jwt 的token本来是去中心化的,现在这样一来,每次请求进来都去调接口验证可以说是违背了去中心化的思想,所以保证性能问题得自己解决;

权限数据

代码语言:javascript
复制
public class PermissionService
{
    /// <summary>
    /// 权限信息(实际上这些应该存在数据库)
    /// </summary>
    public static List<PermissionEntity> Permissions = new List<PermissionEntity>
    {
        //RoleId R01 是管理员,有两个Api的多个接口的权限
        new PermissionEntity{ PermissionId="0001",RoleId="R01", ApiName="Hei.UserApi",Authorised=new Dictionary<string, List<string>>
            {
                { "Profile",new List<string>{ "GetUsername"}},
                { "Credit",new List<string>{ "GetScore"}},
            }
        },
        new PermissionEntity{ PermissionId="0002",RoleId="R01", ApiName="Hei.OrderApi",Authorised=new Dictionary<string, List<string>>
            {
                { "Delivery",new List<string>{ "GetAddress"}},
                { "Order",new List<string>{ "GetOrderNo"}},
            }
        },

        //RoleId R02 是普通员工,有两个Api的多个 部分 接口的权限
        new PermissionEntity{ PermissionId="0001",RoleId="R02", ApiName="Hei.UserApi",Authorised=new Dictionary<string, List<string>>
            {
                { "Profile",new List<string>{ "GetUsername"}},
                //{ "Credit",new List<string>{ "GetScore"}}, //用户信用分接口权限就不给普通员工了
            }
        },
        new PermissionEntity{ PermissionId="0002",RoleId="R02", ApiName="Hei.OrderApi",Authorised=new Dictionary<string, List<string>>
            {
                //{ "Delivery",new List<string>{ "GetAddress"}}, //用户地址信息也是
                { "Order",new List<string>{ "GetOrderNo"}},
            }
        }
    };

当然这些数据一般都是根据你的权限需求存数据库的,与你的权限管理后台相配合;

5、注册自定义授权处理程序

代码语言:javascript
复制
   		/// <summary>
        /// 提交自定义角色的授权策略
        /// </summary>
        /// <param name="services"></param>
        /// <returns></returns>
        public static IServiceCollection AddCustomRBACAuthorizationPolicy(this IServiceCollection services)
        {
            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            services.AddSingleton<IAuthorizationPolicyProvider, CustomRBACPolicyProvider>();
            services.AddSingleton<IAuthorizationHandler, CustomRBACRequirementHandler>();

            return services;
        }

6、在接口上使用自定义授权标签CustomRBACAuthorize

代码语言:javascript
复制
    [Route("api/[controller]/[action]")]
    [ApiController]
    public class CreditController : ControllerBase
    {
        /// <summary>
        /// 获取信用分
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [HttpGet]
        [CustomRBACAuthorize] //这里就表名
        public int GetScore(string id)
        {
            return 666;
        }
    }

7、测试结果

管理员1001 角色id R01 Alice

请求:

可以看到都是 200

普通用户1002 角色id R02 Bob

请求:

可以看到获取用户信用积分、订单投递地址的接口403了,与我们全面的设定相符;

总结

就是一个简单的思路

1、给access_token 带上自定义信息;

2、在客户端重写本地验证/权限校验逻辑即可;

其实token黑白名单,token撤销原理类似 希望能帮上一点小忙;

IdentityServer4就是一个工具,希望大家不要给它设定太多的限制“不能做这个,不能做那个等等”

源码

https://github.com/gebiWangshushu/cnblogs-demos/tree/dev/IdentityServerWithRBAC.Example

如果能有个小星星那就再好不过了(✧◡✧)

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-02-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • 开始之前
  • 实现思路
  • 开始实现
    • 服务端
      • 1、生成自定义token
    • 客户端
      • 1、自定义授权标签CustomRBACAuthorize
      • 2、自定义授权 IAuthorizationRequirement
      • 3、自定义IAuthorizationPolicyProvider
      • 4、自定义Requirement的的 AuthorizationHandler
      • 5、注册自定义授权处理程序
      • 6、在接口上使用自定义授权标签CustomRBACAuthorize
      • 7、测试结果
      • 管理员1001 角色id R01 Alice
      • 普通用户1002 角色id R02 Bob
  • 总结
  • 源码
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档