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的实现逻辑
posted @ 2020-08-07 16:44  shengyu_kmust  阅读(519)  评论(4编辑  收藏  举报