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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM