1. 程式人生 > 實用技巧 >3.通用許可權設計——SnailAspNetCoreFramework快速開發框架之後端設計

3.通用許可權設計——SnailAspNetCoreFramework快速開發框架之後端設計

總體設計思路

在設計本專案的通用許可權前,我參閱過很多設計方案,最終定下RBAC(基於角色的許可權控制)。微軟本身是有一套預設的許可權控制的(asp.net core identity),但有如下幾個缺點
1、表結構固定,不好擴充套件。
2、不能動態的對介面進行角色的授權,只能寫在程式碼裡。所以本框架的設計會考慮如下幾點


  • 不定義表結構,各許可權表的結構完全可由使用者自己定義,只需按規範實現介面即可
  • 能動態分配介面的角色

具體設計摘要

  • 許可權包含人員、角色、人員角色關係、資源、角色資源這5個表,各表不自定具體的結構,只通過介面進行約定
  • 許可權核心邏輯由IPermission和IPermissionStore來定義。
  • IPermission定義了使用者的登入、鑑權、初始化資源、從請求上下文識別出資源id、獲取所有資源及對應角色、登入密碼加密演算法等方法,預設實現為DefaultPermission->BasePermission->IPermission
  • IPermissionStore定義各許可權相關資料的獲取、更新、快取重新整理方法。所有的許可權相關資料都在快取裡,當資料有變化時,要呼叫IPermissionStore的快取重新整理方法來進行。預設實現為DefaultPermissionStore->BasePermissionStore->IPermissionStore
  • 用“基於策略”的方式進行鑑權。策略的實現邏輯為PermissionRequirementHandler,依賴於IPermission,通過IPermission.HasPermission方法來判斷是否有許可權。
  • 支援cookies和jwt兩種方式。在登入時,由
  • 通過在Action上加ResourceAttribute來定義哪些介面是需要進行鑑權的,並自動加入到Resource資料表裡

各表結構的介面約定

  • 人員、角色、人員角色關係、資源、角色資源關係的介面如下
    public interface IHasKeyAndName
    {
        /// <summary>
        /// 一般為id,主鍵
        /// </summary>
        /// <returns></returns>
        string GetKey();
        /// <summary>
        /// 一般為描述
        /// </summary>
        /// <returns></returns>
        string GetName();
    }

人員介面

    public interface IUser:IHasKeyAndName
    {
        string GetAccount();
        string GetPassword();
    }

角色介面

    public interface IRole:IHasKeyAndName
    {
    }

人員角色關係介面

    public interface IUserRole
    {
        string GetUserKey();
        string GetRoleKey();
    }

資源介面

    /// <summary>
    /// 資源(指所有要許可權控制的資源,如介面,選單)
    /// </summary>
    public interface IResource:IHasKeyAndName
    {
        /// <summary>
        /// 用於繫結到前端,前端在做許可權和介面元素的繫結時,一般不會用id(id可讀性差)和name(name可能會改變),一般以code做約定
        /// </summary>
        /// <returns></returns>
        string GetResourceCode();
    }

角色資源關係介面

    public interface IRoleResource
    {
        string GetRoleKey();
        string GetResourceKey();
    }

核心許可權介面定義

IPermission介面定義

    /// <summary>
    /// 許可權介面,這此介面是對外的,非對外的方法,不要寫在接口裡。
    /// </summary>
    public interface IPermission
    {
        #region 用於判斷使用者是否有資源許可權的必要方法
        /// <summary>
        /// 通過訪問的資源,獲取資源的key。如obj可能為action,url
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        string GetRequestResourceKey(object obj);
        /// <summary>
        /// 通過物件獲取資源code
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        string GetRequestResourceCode(object obj);

        /// <summary>
        /// 使用者是否有資源的許可權
        /// </summary>
        /// <param name="resourceKey">資源key</param>
        /// <param name="userKey">使用者key</param>
        /// <returns></returns>
        bool HasPermission(string resourceKey, string userKey);
        /// <summary>
        /// 從ClaimsPrincipal獲取使用者資訊
        /// </summary>
        /// <param name="claimsPrincipal">ClaimsPrincipal</param>
        /// <returns></returns>
        UserInfo GetUserInfo(ClaimsPrincipal claimsPrincipal);
        #endregion

        #region 登入、前端介面許可權控制必要方法

        /// <summary>
        /// 登入
        /// </summary>
        /// <param name="loginDto">登入dto</param>
        /// <returns>如果登入成功,返回的結果;如果登入不成功,會丟擲異常</returns>
        /// <remarks>
        /// 配置GetAllResourceRoles方法,可實現前端的許可權控制
        /// </remarks>
        LoginResult Login(LoginDto loginDto);

        /// <summary>
        /// 獲取所有的資源以及資源角色的對應關係資訊
        /// </summary>
        /// <returns></returns>
        /// <remarks>
        /// 前端呼叫此介面,獲取所有的資源及資源的角色,用於渲染介面許可權控制
        /// </remarks>
        List<ResourceRoleInfo> GetAllResourceRoles();


        /// <summary>
        /// 通過userInfo生成Claims,Claims會用於生成token
        /// </summary>
        /// <param name="userInfo"></param>
        /// <returns></returns>
        List<Claim> GetClaims(IUserInfo userInfo);

        ///// <summary>
        ///// 獲取登入token
        ///// </summary>
        ///// <param name="account"></param>
        ///// <param name="pwd"></param>
        ///// <returns></returns>
        //string GetLoginToken(string account, string pwd);

        ///// <summary>
        ///// 獲取使用者資訊,用於給前端使用者展示
        ///// </summary>
        ///// <param name="token"></param>
        //IUserInfo GetUserInfo(string token);

        #endregion

        #region 其它
        /// <summary>
        /// 獲取password的hash,可能加salt或是不加,hash的演算法也可以由使用者自己配置。
        /// 如果使用者密碼在儲存時不做hash處理,則此方法返回pwd的明文即可
        /// 此方法用於兩處
        /// 1、登入驗證
        /// 2、修改、增加密碼時
        /// </summary>
        /// <param name="pwd">使用者輸入的密碼明文</param>
        /// <returns>密碼明文的hash</returns>
        string HashPwd(string pwd);

        /// <summary>
        /// 初始化許可權資源
        /// </summary>
        void InitResource();
        #endregion

    }

IPermissionStore介面定義

   /// <summary>
    /// 許可權儲存相關的介面約定
    /// </summary>
    public interface IPermissionStore
    {
        #region 查詢許可權資料
        /// <summary>
        /// 獲取所有的使用者
        /// </summary>
        /// <returns></returns>
        List<IUser> GetAllUser();
        /// <summary>
        /// 獲取所有的角色
        /// </summary>
        /// <returns></returns>
        List<IRole> GetAllRole();
        /// <summary>
        /// 獲取所有角色和使用者的關係
        /// </summary>
        /// <returns></returns>
        List<IUserRole> GetAllUserRole();
        /// <summary>
        /// 獲取所有的資源
        /// </summary>
        /// <returns></returns>
        List<IResource> GetAllResource();
        /// <summary>
        /// 獲取所有角色和資源的關係
        /// </summary>
        /// <returns></returns>
        List<IRoleResource> GetAllRoleResource();
        #endregion

        #region 管理許可權資料

        /// <summary>
        /// 儲存使用者
        /// </summary>
        /// <param name="user"></param>
        void SaveUser(IUser user);
        /// <summary>
        /// 刪除使用者
        /// </summary>
        /// <param name="userKey"></param>
        void RemoveUser(string userKey);
        /// <summary>
        /// 儲存角色
        /// </summary>
        /// <param name="role"></param>
        void SaveRole(IRole role);
        /// <summary>
        /// 刪除角色 
        /// </summary>
        /// <param name="roleKey"></param>
        void RemoveRole(string roleKey);
        /// <summary>
        /// 儲存資源
        /// </summary>
        /// <param name="resource"></param>
        void SaveResource(IResource resource);
        /// <summary>
        /// 刪除資源
        /// </summary>
        /// <param name="resourceKey"></param>
        void RemoveResource(string resourceKey);
        /// <summary>
        /// 裝置使用者的角色
        /// </summary>
        /// <param name="userKey"></param>
        /// <param name="roleKeys"></param>
        void SetUserRoles(string userKey, List<string> roleKeys);
        /// <summary>
        /// 設定角色的資源
        /// </summary>
        /// <param name="roleKey">角色key</param>
        /// <param name="resourceKeys">資源keys</param>
        void SetRoleResources(string roleKey, List<string> resourceKeys);

        /// <summary>
        /// IPermissionStore的實現裡如果用了快取,此方法用於重新整理快取為最新資料。
        /// 如果使用者是通過非IPermissionStore介面方法操作許可權資料,則要呼叫此方法進行資料重新整理 
        /// </summary>
        void ReloadPemissionDatas();
        #endregion

    }

怎麼用?

下面按將許可權控制接入到專案的開發步驟進行示例和解讀

1、定義許可權表實體

  • 包含人員、角色、人員角色關係、資源、角色資源這5個表
  • 分別定義User,Role,UserRole,Resource,RoleResource5個實體,分別繼承IUser,IRole,IUserRole,IResource,IRoleResource介面
  • 由於程式碼比較簡單,就不附原始碼了,詳細可以檢視ApplicationCore的Entities資料夾裡的實現定義

2、建立IPermissionStore介面的實現類

  • 資料庫框架用的是entityframework core,將已經定義好的實體加到DbContext裡(參考Infrastracture專案裡的AppDbContext)
  • 為方便擴充套件,我建立了基類BasePermissionStore,並實現IPermissionStore,在專案接入時,可繼承BasePermissionStore類,並實現部分虛方法即可。如DefaultPermissionStore。(由於只是簡單的資料庫CRUD操作,詳細程式碼請檢視Web專案裡Permission裡的程式碼)
  • 預設的實現裡,我加了快取,避免每次許可權驗證時去查庫,並在許可權相關資料改變時清空對應的快取

3、建立IPermission介面的實現類

  • 為方便是快速接入,可繼承BasePermission。或自己實現IPermission介面
  • 本框架預設的實現為DefaultPermission
  • IPermission的大致思路為,用IPermissionStore裡提供的許可權相關資料,判斷使用者的角色,進而知道使用者有哪些授權資源。
  • 附BasePermission和DefaultPermission的原始碼
    BasePermission
    /// <summary>
    /// 許可權控制抽象基類,外部在實現許可權控制時,如果繼承此類,會簡化實現的過程,也可以繼承IPermission介面,自己實現 
    /// </summary>
    /// <remarks>
    /// todo 由於鑑權是頻繁的操作,後期計劃將鑑權方法裡linq相關的操作用hash和快取技術實現,進一步提高效能
    /// </remarks>
    public abstract class BasePermission : IPermission
    {
        protected IPermissionStore _permissionStore;
        protected abstract PermissionOptions PermissionOptions {set;get;}
        public BasePermission(IPermissionStore permissionStore)
        {
            _permissionStore = permissionStore;
        }

        #region 用於判斷使用者是否有資源許可權的必要方法
        public virtual string GetRequestResourceKey(object obj)
        {
            var resourceKey = string.Empty;
            var resourceCode = GetRequestResourceCode(obj);
            if (!string.IsNullOrEmpty(resourceCode))
            {
                resourceKey = _permissionStore.GetAllResource().FirstOrDefault(a => a.GetResourceCode() == resourceCode)?.GetKey();
            }
            return resourceKey;
        }
        public abstract string GetRequestResourceCode(object obj);

        public virtual bool HasPermission(string resourceKey, string userKey)
        {
            var userRoleKeys = _permissionStore.GetAllUserRole().Where(a => a.GetUserKey() == userKey).Select(a => a.GetRoleKey());
            var resource = _permissionStore.GetAllResource().FirstOrDefault(a => a.GetKey() == resourceKey);
            
            //未納入到資源表裡的資源,如果進入到鑑權過程時,不允許訪問。請將不需要做許可權控制的資源設定成允許匿名訪問,避免進入到鑑權流程
            if (resource==null)
            {
                return false;
            }
            var resourceRoleKeys = _permissionStore.GetAllRoleResource().Where(a => a.GetResourceKey() == resource.GetKey()).Select(a => a.GetRoleKey());
            return userRoleKeys.Intersect(resourceRoleKeys).Any();
        }
        public virtual UserInfo GetUserInfo(ClaimsPrincipal claimsPrincipal)
        {
            return new UserInfo
            {
                Account = claimsPrincipal.FindFirst(PermissionConstant.accountClaim)?.Value,
                RoleKeys = (claimsPrincipal.FindFirst(PermissionConstant.roleIdsClaim)?.Value ?? "").Split(',').ToList(),
                RoleNames = (claimsPrincipal.FindFirst(PermissionConstant.rolesNamesClaim)?.Value ?? "").Split(',').ToList(),
                UserKey = claimsPrincipal.FindFirst(PermissionConstant.userIdClaim)?.Value,
                UserName = claimsPrincipal.FindFirst(PermissionConstant.userNameClaim)?.Value,
            };
        }
        #endregion

        #region 登入、前端介面許可權控制必要方法
        /// <summary>
        /// 登入,返回使用者的基本資訊和token
        /// </summary>
        /// <param name="loginDto">登入dto</param>
        /// <returns>使用者的基本資訊和token物件</returns>
        public virtual LoginResult Login(LoginDto loginDto)
        {
            var user = _permissionStore.GetAllUser().FirstOrDefault(a => a.GetAccount().Equals(loginDto.Account,StringComparison.OrdinalIgnoreCase));
            if (user != null && HashPwd(loginDto.Pwd).Equals(user.GetPassword(),StringComparison.OrdinalIgnoreCase))
            {
                var roleKeys = _permissionStore.GetAllUserRole().Where(a => a.GetUserKey() == user.GetKey()).Select(a => a.GetRoleKey()) ?? new List<string>();
                var roleNames = _permissionStore.GetAllRole().Where(a => roleKeys.Contains(a.GetKey())).Select(a => a.GetName()) ?? new List<string>();
                var userInfo = new UserInfo
                {
                    Account = user.GetAccount(),
                    RoleKeys = roleKeys.ToList(),
                    RoleNames = roleNames.ToList(),
                    UserKey = user.GetKey(),
                    UserName = user.GetName()
                };
                var claims = GetClaims(userInfo);
                var tokenStr= GenerateTokenStr(claims);
                return new LoginResult
                {
                    Token = tokenStr,
                    UserInfo = userInfo
                };
            }
            else
            {
                throw new BusinessException($"使用者名稱或密碼錯誤");
            }
        }
        public virtual List<ResourceRoleInfo> GetAllResourceRoles()
        {
            var result = new List<ResourceRoleInfo>();
            var allResource = _permissionStore.GetAllResource();
            var allRole = _permissionStore.GetAllRole();
            var allRoleResource = _permissionStore.GetAllRoleResource();
            allResource.ForEach(resource =>
            {
                var resourceRoleKeys = allRoleResource.Where(a => a.GetResourceKey() == resource.GetKey()).Select(a => a.GetRoleKey()).Distinct().ToList();
                result.Add(new ResourceRoleInfo
                {
                    ResourceCode=resource.GetResourceCode(),
                    ResourceKey=resource.GetKey(),
                    ResourceName=resource.GetName(),
                    RoleKeys= resourceRoleKeys
                });
            });
            return result;
        }
        public virtual List<Claim> GetClaims(IUserInfo userInfo)
        {
            return new List<Claim>
            {
                new Claim(PermissionConstant.userIdClaim,userInfo.UserKey),
                new Claim(PermissionConstant.userNameClaim,userInfo.UserName),
                new Claim(PermissionConstant.accountClaim,userInfo.Account),
                new Claim(PermissionConstant.roleIdsClaim,string.Join(",",userInfo.RoleKeys??new List<string>()) ),
                new Claim(PermissionConstant.rolesNamesClaim,string.Join(",",userInfo.RoleNames??new List<string>()) ),
            };
        }
        #endregion




        /// <summary>
        /// 預設的密碼hash演算法
        /// </summary>
        /// <param name="pwd">密碼明文</param>
        /// <returns></returns>
        public virtual string HashPwd(string pwd)
        {
            return BitConverter.ToString(HashAlgorithm.Create(HashAlgorithmName.MD5.Name).ComputeHash(Encoding.UTF8.GetBytes(pwd))).Replace("-", "");
        }
   
        public abstract string GenerateTokenStr(List<Claim> claims);

        public abstract void InitResource();
        
    }

DefaultPermission

 /// <summary>
    /// 許可權的預設實現類
    /// </summary>
    public class DefaultPermission : BasePermission
    {
        public static readonly string superAdminRoleName = "SuperAdmin";

        protected override PermissionOptions PermissionOptions { get; set; }

        public DefaultPermission(IPermissionStore permissionStore, IOptionsMonitor<PermissionOptions> permissionOptions) : base(permissionStore)
        {
            PermissionOptions = permissionOptions.CurrentValue ?? new PermissionOptions();
        }

        public override bool HasPermission(string resourceKey, string userKey)
        {
            if (IsSuperAdmin(userKey))
            {
                return true;
            }
            return base.HasPermission(resourceKey, userKey);
        }

        public override string GenerateTokenStr(List<Claim> claims)
        {
            var expireTimeSpan = (PermissionOptions.ExpireTimeSpan == null || PermissionOptions.ExpireTimeSpan == TimeSpan.Zero) ? new TimeSpan(6, 0, 0) : PermissionOptions.ExpireTimeSpan;
            SigningCredentials creds;
            if (PermissionOptions.IsAsymmetric)
            {
                var key = new RsaSecurityKey(RSAHelper.GetRSAParametersFromFromPrivatePem(PermissionOptions.RsaPrivateKey));
                creds = new SigningCredentials(key, SecurityAlgorithms.RsaSha256);
            }
            else
            {
                var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(PermissionOptions.SymmetricSecurityKey));
                creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            }
            var token = new JwtSecurityToken(PermissionOptions.Issuer, PermissionOptions.Audience, claims, DateTime.Now, DateTime.Now.Add(expireTimeSpan), creds);
            var tokenStr = new JwtSecurityTokenHandler().WriteToken(token);
            return tokenStr;
        }
        public override string HashPwd(string pwd)
        {
            return HashHelper.Md5($"{pwd}{PermissionOptions.PasswordSalt}");
        }

        /// <summary>
        /// 獲取資源物件的code,已經適配如下型別:AuthorizationFilterContext,ControllerActionDescriptor,methodInfo
        /// 預設為className_methodName,或是resourceAttribute裡設定的code
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public override string GetRequestResourceCode(object obj)
        {
            if (obj is MethodInfo)
            {
                return GetResourceCode((MethodInfo)obj);
            }
            MethodInfo methodInfo;
            if (obj is AuthorizationFilterContext authorizationFilterContext)
            {
                if (authorizationFilterContext.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
                {
                    methodInfo = controllerActionDescriptor.MethodInfo;
                    return GetResourceCode(methodInfo);
                    //resourceCode = GetResourceCode(controllerActionDescriptor.ControllerName, controllerActionDescriptor.ActionName);
                }
            }
            if (obj is ControllerActionDescriptor controllerActionDescriptor1)
            {
                methodInfo = controllerActionDescriptor1.MethodInfo;
                return GetResourceCode(methodInfo);
                //resourceCode = GetResourceCode(controllerActionDescriptor1.ControllerName, controllerActionDescriptor1.ActionName);
            }

            if (obj is RouteEndpoint endpoint)
            {
                //.net core 3.1後,AuthorizationHandlerContext.Resource為endpoint
                methodInfo = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>()?.MethodInfo;
                return GetResourceCode(methodInfo);

            }
            return string.Empty;
        }

        /// <summary>
        /// 初始化所有的許可權資源。
        /// 所有有定義ResourceAttribute的方法都為許可權資源,否則不是。要使方法受許可權控制,必須做到如下兩點:1、在方法上加ResourceAttribute,2、在controller或是action上加Authorize
        /// </summary>
        public override void InitResource()
        {
            var resources = new List<Resource>();
            if (PermissionOptions.ResourceAssemblies == null)
            {
                PermissionOptions.ResourceAssemblies = new List<Assembly>();
            }
            var existResources = _permissionStore.GetAllResource();
            PermissionOptions.ResourceAssemblies.Add(this.GetType().Assembly);
            PermissionOptions.ResourceAssemblies?.Distinct().ToList().ForEach(assembly =>
            {
                //對所有的controller類進行掃描
                assembly.GetTypes().Where(type => typeof(ControllerBase).IsAssignableFrom(type)).ToList().ForEach(controller =>
                {
                    var controllerIsAdded = false;//父是否增加
                    var parentId = IdGenerator.Generate<string>();
                    var parentResource = controller.GetCustomAttribute<ResourceAttribute>();
                    controller.GetMethods().ToList().ForEach(method =>
                    {
                        if (method.IsDefined(typeof(ResourceAttribute), true))
                        {
                            var methodResource = method.GetCustomAttribute<ResourceAttribute>();
                            if (!controllerIsAdded)
                            {
                                // 增加父
                                resources.Add(new Resource
                                {
                                    Id = parentId,
                                    Code = parentResource?.ResourceCode??controller.Name,
                                    CreateTime = DateTime.Now,
                                    IsDeleted = false,
                                    Name = parentResource?.Description??controller.Name
                                });
                                controllerIsAdded = true;
                            }
                            // 增加子
                            resources.Add(new Resource
                            {
                                Id = IdGenerator.Generate<string>(),
                                Code = GetResourceCode(method),
                                CreateTime = DateTime.Now,
                                IsDeleted = false,
                                ParentId = parentId,
                                Name = methodResource?.Description??method.Name
                            });
                        }
                    });
                });
            });
            resources.ForEach(item =>
            {
                var temp = new Resource
                {
                    Id = item.Id,
                    Code = item.Code,
                    CreateTime = DateTime.Now,
                    IsDeleted = false,
                    Name = item.Name,
                    ParentId = item.ParentId,
                    UpdateTime = DateTime.Now
                };
                // 設定資源的id
                var matchRs = existResources.FirstOrDefault(i => i.GetResourceCode() == temp.Code);
                if (matchRs!=null)
                {
                    temp.Id = matchRs.GetKey();
                }

                // 設定資源的父id
                if (!string.IsNullOrEmpty(temp.ParentId))
                {
                    var pa = resources.FirstOrDefault(a => a.Id == temp.ParentId);
                    var matchPa = existResources.FirstOrDefault(i => i.GetResourceCode() == pa?.Code);
                    if (matchPa!=null)
                    {
                        item.ParentId = matchPa.GetKey();
                    }
                }
                _permissionStore.SaveResource(item);
            });
        }

        private bool IsSuperAdmin(string userKey)
        {
            var superRole = _permissionStore.GetAllRole().FirstOrDefault(a => a.GetName().Equals(DefaultPermission.superAdminRoleName,StringComparison.OrdinalIgnoreCase));
            return _permissionStore.GetAllUserRole().Any(a => a.GetUserKey() == userKey && a.GetRoleKey() == superRole.GetKey());
        }

        /// <summary>
        /// 通過類名和方法名,獲取
        /// </summary>
        /// <param name="className"></param>
        /// <param name="methodName"></param>
        /// <returns></returns>
        private string GetResourceCode(MethodInfo methodInfo)
        {
            if (Attribute.IsDefined(methodInfo, typeof(ResourceAttribute)))
            {
                var attr = methodInfo.GetCustomAttribute<ResourceAttribute>();
                if (attr != null && !string.IsNullOrEmpty(attr.ResourceCode))
                {
                    return attr.ResourceCode;
                }
            }
            return $"{methodInfo.DeclaringType.Name.Replace("Controller", "")}_{methodInfo.Name}";
        }

    }

4、編寫鑑權處理類PermissionRequirementHandler

  • 鑑權的原理請參考微軟的官方文件https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/introduction?view=aspnetcore-3.1
  • 原理概要解說

我用的是基於策略的鑑權方式,一個專案裡可以有多種方法(策略)來判斷一個資源是否有訪問許可權。策略的實現裡是以“是否獲得某個Requirement”來判斷是否有許可權,獲得Requirement即有授權,反之則無。而判斷是否有某某Requirement是由此Requirement的AuthorizationHandler來處理

配置策略

  services.AddAuthorization(options =>
            {
                // 增加鑑權策略,並告知這個策略要判斷使用者是否獲得了PermissionRequirement這個Requirement
                options.AddPolicy(PermissionConstant.PermissionAuthorizePolicy, policyBuilder =>
                {
                    policyBuilder.AddRequirements(new PermissionRequirement());
                });
            });

PermissionRequirementHandler原始碼如下

 public class PermissionRequirementHandler : AuthorizationHandler<PermissionRequirement>
    {
        private IPermission _permission;
        public PermissionRequirementHandler(IPermission permission)
        {
            _permission = permission;
        }
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
        {
            var resourceKey=_permission.GetRequestResourceKey(context.Resource);// 獲取資源的key
            var userKey = _permission.GetUserInfo(context.User).UserKey; // 根據使用者的claims獲取使用者的key
            if (_permission.HasPermission(resourceKey,userKey)) // 判斷使用者是否有許可權
            {
                context.Succeed(requirement); // 如果有許可權,則獲得此Requirement
            }
            return Task.CompletedTask;
        }
    }

5、配置身份驗證和許可權驗證

  • 在Startup.cs裡注入許可權元件services.AddPermission,並在asp.net core管理裡配置好身份驗證和鑑權,即增加 app.UseAuthentication()和app.UseAuthorization();
  • 預設的身份驗證現實支援cookies和token,當請求過來時,如果不包含token則走cookies方式,否則走token。
    AddPermission原始碼如下
  /// <summary>
        /// 許可權控制核心,即必須的配置
        /// </summary>
        /// <param name="services"></param>
        /// <param name="action"></param>
        public static void AddPermission(this IServiceCollection services, Action<PermissionOptions> action)
        {
            services.TryAddScoped<IPermission, DefaultPermission>();
            services.TryAddScoped<IPermissionStore, DefaultPermissionStore>();
            #region 身份驗證
            var permissionOption = new PermissionOptions();
            action(permissionOption);
            //addAuthentication不放到AddPermissionCore方法裡,是為了外部可自己配置
            // 當未通過authenticate時(如無token或是token出錯時),會返回401,當通過了authenticate但沒通過authorize時,會返回403。
            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
               .AddCookie(
                   CookieAuthenticationDefaults.AuthenticationScheme, options =>
                   {
                   //下面的委託方法只會在第一次cookie驗證時呼叫,呼叫時會用到上面的permissionOption變數,但其實permissionOption變數是在以前已經初始化的,所以在此方法呼叫之前,permissionOption變數不會被釋放
                   options.Cookie.Name = "auth";
                       options.AccessDeniedPath = permissionOption.AccessDeniedPath;
                       options.LoginPath = permissionOption.LoginPath;
                       options.ExpireTimeSpan = permissionOption.ExpireTimeSpan != default ? permissionOption.ExpireTimeSpan : new TimeSpan(12, 0, 0);
                       options.ForwardDefaultSelector = context =>
                       {
                           string authorization = context.Request.Headers["Authorization"];
                           //身份驗證的順序為jwt、cookie
                           if (authorization != null && authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
                           {
                               return JwtBearerDefaults.AuthenticationScheme;
                           }
                           else
                           {
                               return CookieAuthenticationDefaults.AuthenticationScheme;
                           }
                       };
                       var cookieAuthenticationEvents = new CookieAuthenticationEvents
                       {
                           OnSignedIn = context =>
                           {
                               return Task.CompletedTask;
                           },
                           OnSigningOut = context =>
                           {
                               return Task.CompletedTask;
                            }
                       };
                       options.Events = cookieAuthenticationEvents;
                   })
               .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
               {
                   // jwt可用對稱和非對稱演算法進行驗籤
                   SecurityKey key;
                   if (permissionOption.IsAsymmetric)
                   {
                       key = new RsaSecurityKey(RSAHelper.GetRSAParametersFromFromPublicPem(permissionOption.RsaPublicKey));
                   }
                   else
                   {
                       key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(permissionOption.SymmetricSecurityKey));
                   }
                   options.TokenValidationParameters = new TokenValidationParameters()
                   {

                       NameClaimType = PermissionConstant.userIdClaim,
                       RoleClaimType = PermissionConstant.roleIdsClaim,
                       ValidIssuer = permissionOption.Issuer,
                       ValidAudience = permissionOption.Audience,
                       IssuerSigningKey = key,
                       ValidateIssuer = false,
                       ValidateAudience = false
                   };
                   var jwtBearerEvents = new JwtBearerEvents
                   {
                       OnMessageReceived = context =>
                       {
                           return Task.CompletedTask;
                       },
                       OnTokenValidated = context =>
                       {
                           return Task.CompletedTask;
                       },
                       OnAuthenticationFailed = context =>
                       {
                           return Task.CompletedTask;
                       }

                   };
                   options.Events = jwtBearerEvents;
               });
            #endregion
            #region 授權

            //許可權控制只要在配置IServiceCollection,不需要額外配置app管道
            //許可權控制參考:https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-2.2
            //handler和requirement有幾種關係:1 handler對多requirement(此時handler實現IAuthorizationHandler);1對1(實現AuthorizationHandler<PermissionRequirement>),和多對1
            //所有的handler都要注入到services,用services.AddSingleton<IAuthorizationHandler, xxxHandler>(),而哪個requirement用哪個handler,低層會自動匹配。最後將requirement對到policy裡即可
            services.AddAuthorization(options =>
            {
                options.AddPolicy(PermissionConstant.PermissionAuthorizePolicy, policyBuilder =>
                {
                    policyBuilder.AddRequirements(new PermissionRequirement());
                });
            });
            services.AddScoped<IAuthorizationHandler, PermissionRequirementHandler>();
            services.AddMemoryCache();
            services.TryAddScoped<IApplicationContext, ApplicationContext>();
            services.AddHttpContextAccessor();
            services.Configure(action);
            #endregion
        }

6、在需要進行許可權控制的action或是Controller上加Authorize特性

[Authorize(Policy = PermissionConstant.PermissionAuthorizePolicy)]

其它要點

如何根據程式碼的介面自動生成許可權資源

  • IPermission接口裡定義了InitResource方法,此方法即是自動生成許可權資源的入口。
  • 預設將所有的Controller裡的Action設定為許可權資源,如果不需要,則加上AllowAnonymous特性可即
  • 資源的code為ControllerName_ActionName,描述資訊可以用Resource特性來定義
  • 參考DefaultPermission.InitResource的實現邏輯