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的實現邏輯


免責聲明!

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



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