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


    img

    写在前面#

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

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

    “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 验证交互过程图

    img

    image-20220223112832900

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

    开始实现#

    服务端#

    1、生成自定义token#

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

    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 启用

    builder.AddResourceOwnerValidator<CustomResourceOwnerPasswordValidator>();
    builder.AddProfileService<CustomProfileService>();
    

    2、请求一个token来看看:

    image-20220223115450490

    image-20220223115310375

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

    客户端#

    1、自定义授权标签CustomRBACAuthorize#

        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#

       public class CustomRBACRequirement: IAuthorizationRequirement
        {
            public string PolicyName { get; }
    
            public CustomRBACRequirement(string policyName)
            {
                this.PolicyName = policyName;
            }
        }
    

    3、自定义IAuthorizationPolicyProvider#

    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#

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

    权限数据

    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、注册自定义授权处理程序#

       		/// <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#

        [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#

    image-20220223151041196

    请求:

    image-20220223152252049

    可以看到都是 200

    普通用户1002 角色id R02 Bob#

    image-20220223151144846

    请求:

    image-20220223152233656

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

    总结#

    就是一个简单的思路

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

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

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

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

    源码#

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

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

    参考#

    https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/iauthorizationpolicyprovider?view=aspnetcore-3.1#multiple-authorization-policy-providers

    https://www.cnblogs.com/stulzq/p/9226059.html

    👇专注于后端和架构,欢迎扫码关注我的公众号👇
  • 相关阅读:
    改进YOLOv7系列:最新结合DO-DConv卷积、Slim范式提高性能涨点,打造高性能检测器
    Web Woeker和Shared Worker的使用以及案例
    “比特”与“瓦特”深度融合,云计算驶向绿色低碳快车道
    总结:vue的通信方式
    俄罗斯 Android 系统受限,或将转用 HarmonyOS?
    学好Python-新手小白如何做?
    SAP 电商云 Spartacus UI 和 Accelerator UI 里的 ASM 模块
    第二十六章CSS3续~
    CURL踩坑记录
    【c++ primer 笔记】第13章 拷贝控制
  • 原文地址:https://www.cnblogs.com/xiaxiaolu/p/15929063.html