尝试封装适用于权限管理的通用API

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了尝试封装适用于权限管理的通用API相关的知识,希望对你有一定的参考价值。

谈谈我对权限系统的简单理解

最近一段时间在研究权限系统,在园子里看到个很牛逼的开源的基于DDD-Lite的权限管理系统,并有幸加入了作者的QQ群,呵呵,受到了很大的影响。对于权限管理我有我自己的一些简单想法,说简单也许是因为我对权限管理领域的理解有限,我所做的仅是基于我本人对权限管理的理解。以最终目的为出发点,应该就是一个系统根据不同的登陆用户的不同权限而呈现不同的界面,不同的操作,数据和资源等等。从实现方面大体可以分几个部分:用户登陆管理部分,用户权限分配部分,响应终端或界面部分。目前我已经根据我自己的上述理解,最简单的实现了前两个部分。

 

看看我的框架是否合理

技术分享

我的Domain层主要分了两个部分,一个是RBAC,一个是原有的一些基础架构,RBAC是后面加进去的,完全可以是独立的一个库,不依赖任何第三方框架和数据库。主要功能就是为任何一个实体绑定权限或是资源,提供实体的权限查询和权限检测,这个实体不限于用户或角色

技术分享

这里其实有点过度了,在现有框架中接口完全可以无视的。因为这个RBAC不依赖于其它框架或其它层,它又在Domain层,其它层可以直接拿去用。

首先说说模型,有三个模基本模型,BaseEntity是基础实体模型,主要统一标识类型和设置id唯一属性;BaseRelevance是最关健的模型,用于记录一个实体对另一个实体或是权限或是资源的关联关系,对这个模型的所有属性除了ID其它都从代码逻辑中赋值。其中有一个属性对应了一个枚举类PermissionType,用于记录权限的类型。另一个BaseHideFiled用于实现某些实体的字段隐藏权限,主要记录要隐藏字段的实体的ID和字段名称。下面把代码贴出来大家看看

技术分享
namespace YZY.Domain
{
    /// <summary>
    /// 权限类型
    /// </summary>
    public enum PermissionType
    {
        /// <summary>
        /// 管理员操作权限
        /// </summary>
        AdmainAction,
        /// <summary>
        /// 角色
        /// </summary>
        Role,
        /// <summary>
        /// 组资源
        /// </summary>
        Group,
        /// <summary>
        /// 功能模块或菜单
        /// </summary>
        Module,
        /// <summary>
        /// 操作
        /// </summary>
        Action,
        /// <summary>
        /// 按钮
        /// </summary>
        Button,
        /// <summary>
        /// 控制器
        /// </summary>
        Contorller,
        /// <summary>
        /// 属性
        /// </summary>
        Propety,
        /// <summary>
        /// 数据
        /// </summary>
        Data,
        /// <summary>
        /// 用户
        /// </summary>
        User,
        /// <summary>
        /// 其它
        /// </summary>
        Other
    }
}
枚举类
技术分享
namespace YZY.Domain
{
    public abstract class BaseEntity<TKey> : IEntity<TKey>
    {
        #region 初始化
        public TKey Id { get; set; }
        protected BaseEntity(TKey id)
        {
            this.Id = id;
        }
        #endregion 
    }
    
}
基类
技术分享
namespace YZY.Domain
{
    public abstract class BaseRelevance<TKey>:BaseEntity<TKey>,IRelevance<TKey>
    {
        protected BaseRelevance(TKey id) : base(id) { }

        /// <summary>
        /// 权限类型
        /// </summary>
        public string PermissionType { get; set; }
        /// <summary>
        /// 实体的id
        /// </summary>
        public TKey EntityId { get; set; }
        /// <summary>
        /// 实体类型
        /// </summary>
        public string EntityType { get; set; }
        /// <summary>
        /// 权限id
        /// </summary>
        public TKey PermisId { get; set; }
    }
}
关联类
技术分享
namespace YZY.Domain
{
    public abstract class BaseHideFiled<TKey>:BaseEntity<TKey>,IHideFiled<TKey>
    {
        protected BaseHideFiled(TKey id) : base(id)
        {

        }
        public TKey EntityId { get; set; }
        public string HidePropertyName { get; set; }
        public string EntityType { get; set; }
        
    }
}
隐藏字段类

 基础功能都在RBACManage类中实现,由于没有依赖数据库,所以没有实体绑定权限持久化的具体实现,相关的方法返回一个关联类的类型实例,以供有数据库的层(比如服务层)持久化。呵呵,说服务层有数据库其实是不对的,因为服务层其实只是调用Domain层的接口提供的方法而已,具体怎么实现,它并不知道。这里顺便以领域层,服务层和数据库层关系说说我对分层和依赖注入的简单理解,Domain应该是根据业务需要设计出相应的逻辑实现,但是数据必须持久化,而对于数据库或是ORM框架是个可变化点,为了响应这个变化点而设计出专门针对数据库的接口,你可以以不同的ORM来实现这个接口,比如EF或NH或是ADO.NET。服务层中要用到Domain层的接口,但是必须要有个具体实现来操作,这时候就得用上依赖注入了。层之间的关系就变成了服务层引用Domain,调用它的接口,数据库层也引用Domain层,实现它的接口,服务层不引用数据库层却通过接口和注入间接调用了数据库层。RBACManage代码后面再奉上。这里主要先看看我的框架怎么样。

技术分享

基础服务中,UserManage主要负责对用户的基础管理,有几个静态方法供其它层调用,目前我只写了登陆管理(cookie+cache),密码加密解密,其它功能还没有写;CustomWarring是一个自定义异常类,主要方便异常管理。DependencyContext依赖注入上下文,通过这个类,可以在其它层使用不同的依赖注入组件,比如可以用autofac,也可以用其它。pager是我自己写的一个超简单却实用的分页类。其它一些接口加上依赖注入组件主要用于隔离层与层之间的依赖,或方便一个基础功能可以有不同的具体实现的变化点。模型目前主要是一些简单的权限管理模型,方便测试。对于框架的结构,这里不能讲太多,否则跑题了。如果有时间,或是大家觉得可参考,再搞一篇文章详细谈谈

 

试试我的权限操作类

技术分享
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace YZY.Domain
{

    /// <summary>
    ///  权限管理操作
    /// </summary>
    /// <typeparam name="TEntity">实体类</typeparam>
    /// <typeparam name="TPermission">权限类</typeparam>
    public class RBACManage<TEntity,TKey> : IRBACManage<TEntity, TKey>
        where TEntity : BaseEntity<TKey>
    {               
        public  IQueryable<BaseRelevance<TKey>> RelevanceQuery { get; set; }
        public  IQueryable<BaseHideFiled<TKey>> HideFiledQuery { get; set; }

        /// <summary>
        /// 仓储检测
        /// </summary>
        private void CheckQuery()
        {
            if (RelevanceQuery == null)
                throw new Exception("没有对应的仓储");
        }        

        /// <summary>
        /// 获取一个实体绑定一个权限后的关联类
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="newRelvance"></param>
        /// <param name="permis"></param>
        /// <param name="perType"></param>
        /// <returns></returns>
        public BaseRelevance<TKey> GetBindRelevance<TPermission>
            (TEntity entity,
            BaseRelevance<TKey> newRelvance,
            TPermission permis, 
            PermissionType perType)
            where TPermission : BaseEntity<TKey>
        {            
            if (permis == null) return null;
            newRelvance.EntityId = entity.Id;
            newRelvance.EntityType = entity.GetType().Name;
            newRelvance.PermisId = permis.Id;
            newRelvance.PermissionType = perType.ToString();
            return newRelvance;
        }
        
        /// <summary>
        /// 将一个实体的某个字段注册成隐藏字段权限并返回这个权限类型
        /// </summary>
        /// <typeparam name="TFiled"></typeparam>
        /// <param name="entity"></param>
        /// <param name="filed"></param>
        /// <param name="newHide">new一个即可</param>
        /// <returns></returns>
        public BaseHideFiled<TKey> GetRegisteHideFiled<TFiled>
            (TEntity entity,Expression<Func<TEntity,TFiled>>filed,BaseHideFiled<TKey>newHide)
        {
            newHide.EntityId = entity.Id;
            newHide.HidePropertyName = LambdExt.GetName(filed);
            newHide.EntityType = entity.GetType().Name;
            return newHide;
        }
        /// <summary>
        /// 将一个实体的某个字段注册成隐藏字段权限并返回这个权限类型
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="filedName"></param>
        /// <param name="newHide"></param>
        /// <returns></returns>
        public BaseHideFiled<TKey> GetRegisteHideFiled(TEntity entity,string filedName,BaseHideFiled<TKey>newHide)
        {
            newHide.EntityId = entity.Id;
            newHide.HidePropertyName = filedName;
            newHide.EntityType = entity.GetType().Name;
            return newHide;
        }

        /// <summary>
        /// 将一个实体的隐藏字段权限绑定到其它实体的关联类型
        /// 比如可以将一个用户的某个字段对其它用户或角色隐藏
        /// </summary>
        /// <typeparam name="TFiled">字段类型</typeparam>
        /// <typeparam name="TOherEntity">其它实体类型</typeparam>
        /// <param name="newHide">new一个即可</param>
        /// <param name="entity">当前实体</param>
        /// <param name="newRelvance">new一个即可</param>
        /// <param name="otherEntitiy">其它实体</param>
        /// <param name="filed">字段名称</param>
        /// <returns></returns>
        public BaseRelevance<TKey> GetBindHideFiledToOtherEntityRelevance<TOherEntity, TFiled>
            (TEntity entity,
            BaseRelevance<TKey> newRelvance,
            TOherEntity otherEntitiy,
            Expression<Func<TEntity, TFiled>> filed)
            where TOherEntity : BaseEntity<TKey>
        { 
            //从隐藏字段权限的集合中找出这个权限 ,如果不存在报异常
            BaseHideFiled<TKey> hide = null;
            try
            {
                hide = HideFiledQuery.ToList().Where
                        (o => o.EntityId.Equals(entity.Id) && o.HidePropertyName == LambdExt.GetName(filed)).First();
            }
            catch 
            {
                throw new Exception("当前实体还没有注册指定的字段隐藏权限,请先将其注册成隐藏字段权限");
            }
            if (HideFiledQuery == null) throw new Exception("权限仓储为空");
            newRelvance.EntityId = otherEntitiy.Id;
            newRelvance.PermisId = hide.Id;
            newRelvance.EntityType = otherEntitiy.GetType().Name;
            newRelvance.PermissionType = PermissionType.Propety.ToString();                       

            return newRelvance; 
        }

        /// <summary>
        /// 将一个实体的隐藏字段权限绑定到其它实体的关联类型
        /// 比如可以将一个用户的某个字段对其它用户或角色隐藏
        /// </summary>
        /// <typeparam name="TOther">其它实体类型</typeparam>
        /// <param name="entity">当前实体</param>
        /// <param name="newRelvance">new一个即可</param>
        /// <param name="other">其它实体</param>
        /// <param name="filedName">字段名称</param>
        /// <returns></returns>
        public BaseRelevance<TKey> GetBindHideFiledToOtherEntityRelevance<TOther>
            (TEntity entity,BaseRelevance<TKey>newRelvance,TOther other,string filedName)
            where TOther:BaseEntity<TKey>
        {
            //从隐藏字段权限的集合中找出这个权限 ,如果不存在报异常
            BaseHideFiled<TKey> hide = null;
            try
            {
                hide = HideFiledQuery.ToList().Where
                        (o => o.EntityId.Equals(entity.Id) && o.HidePropertyName == filedName).First();
            }
            catch
            {
                throw new Exception("当前实体还没有注册指定的字段隐藏权限,请先将其注册成隐藏字段权限");
            }
            if (HideFiledQuery == null) throw new Exception("权限仓储为空");
            newRelvance.EntityId = other.Id;
            newRelvance.PermisId = hide.Id;
            newRelvance.EntityType = other.GetType().Name;
            newRelvance.PermissionType = PermissionType.Propety.ToString();

            return newRelvance;
        }

        /// <summary>
        /// 检测一个实体是否有指定的权限
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="permis"></param>
        /// <returns></returns>
        public bool ExitsPermission<TPermission>
            (TEntity entity, TPermission permis) where TPermission : BaseEntity<TKey>
        {
            return RelevanceQuery.AsEnumerable().Any(o => o.EntityId.Equals(entity.Id) && o.PermisId.Equals(permis.Id));
        }

        /// <summary>
        /// 获取实体对应的权限集合
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="filter"></param>
        /// <returns></returns>
        public IEnumerable<TPermission> GetPermissions<TPermission>
            (TEntity entity,
            IQueryable<TPermission> permisQuery,
            Func<BaseRelevance<TKey>,bool>relevanceFilter=null,
            Func<TPermission, bool> perFilter = null)
            where TPermission : BaseEntity<TKey>
        {
            var ids = FindPermKeys(entity,relevanceFilter);
            if (permisQuery == null) throw new Exception("权限仓储为空");
            var all = new List<TPermission>();
            ids.ToList().ForEach(o => all.Add(permisQuery.ToList().Where(e => e.Id.Equals(o)).First()));
            if (perFilter == null) return all;
            return all.Where(perFilter);            
        }

        /// <summary>
        /// 获取实体对应的隐藏字段权限
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="hideFilter"></param>
        /// <returns></returns>
        public IEnumerable<BaseHideFiled<TKey>>GetHideFileds
            (TEntity entity,Func<BaseHideFiled<TKey>,bool>hideFilter=null)
        {
            var ids = FindPermKeys(entity, o => o.PermissionType == PermissionType.Propety.ToString());

            if (HideFiledQuery==null) throw new Exception("权限仓储为空");
            var all = new List<BaseHideFiled<TKey>>();
            ids.ToList().ForEach(o => all.Add(HideFiledQuery.ToList().Where(e => e.Id.Equals(o)).First()));
            if (hideFilter == null) return all;
            return all.Where(hideFilter);
        }

        /// <summary>
        /// 获取实体对应的全部权限id
        /// </summary>
        /// <param name="entity"></param>
        /// <returns></returns>
        public IEnumerable<TKey> FindPermKeys(TEntity entity,Func<BaseRelevance<TKey>,bool>filter=null)
        {
            CheckQuery();
            var all= RelevanceQuery.ToList().Where(o => o.EntityId.Equals(entity.Id));
            if (filter != null) return all.Where(filter).Select(o => o.PermisId);
            return all.Select(o => o.PermisId);
            
        }

        /// <summary>
        /// 按权限类型获取实体的权限集合
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="type"></param>
        /// <param name="filter"></param>
        /// <returns></returns>
        public IEnumerable<TPermission> GetPermissionsForType<TPermission>
            (TEntity entity,
            PermissionType type,
            IQueryable<TPermission> PermisQuery,
            Func<TPermission, bool>filter = null)
            where TPermission : BaseEntity<TKey>
        {
            return GetPermissions(entity,PermisQuery, o => o.PermissionType == type.ToString(), filter);
        }            

        /// <summary>
        /// 找出实体对应隐藏字段权限指定类型集合
        /// 比找出一个用户或角色对应的所有隐藏字段的权限,然后将它们转成对应的隐藏字段后的实体
        /// </summary>
        /// <typeparam name="TOther">任意类型</typeparam>
        /// <param name="entity">当前实体</param>
        /// <param name="otherQuery"></param>
        /// <returns></returns>
        public IEnumerable<TOther> GetHideFiledPermissions<TOther>
            (TEntity entity, IQueryable<TOther> otherQuery)
            where TOther : BaseEntity<TKey>
        {

            //获取实体对应的所有隐藏字段类型的权限
            var hides = GetHideFileds(entity);

            //找出TOther类型的权限集合
            if (hides == null) return null;
            var others = hides.Where(o => o.EntityType == typeof(TOther).Name);

            //根据id进行分组
            var look = others.ToLookup(o => o.EntityId, o => o.HidePropertyName);
            List<TOther> list = new List<TOther>();
            //List<Guid> hideIds = new List<Guid>();
            foreach (var item in look)
            {
                //得到这个类型下的所有id
                var other = otherQuery.ToList().Where(o => o.Id.Equals(item.Key)).FirstOrDefault();
                
                List<string> proNames = new List<string>();
                foreach (var fi in item)
                {
                    proNames.Add(fi);
                }
                list.Add(PropertyValueHelper.GetChangeProValueToDefault(other, proNames));
            }
            return list;
        }
    }


}
View Code

哦,这里还用到了两个帮助类,一个是用于获取属性名,一个是用于更改对应属性名的值。其实也就是两个反射方法封装一下。

技术分享
using System;
using System.Linq.Expressions;

namespace YZY.Domain
{
    public  class LambdExt
    {
        /// <summary>
        /// 获取成员名称,范例:t => t.Name,返回 Name
        /// </summary>
        /// <param name="expression">表达式,范例:t => t.Name</param>
        public static string GetName(LambdaExpression expression)
        {
            var memberExpression = GetMemberExpression(expression);
            if (memberExpression == null)
                return string.Empty;
            string result = memberExpression.ToString();
            return result.Substring(result.IndexOf(".", StringComparison.Ordinal) + 1);
        }

        /// <summary>
        /// 获取成员表达式
        /// </summary>
        private static MemberExpression GetMemberExpression(LambdaExpression expression)
        {
            if (expression == null)
                return null;
            var unaryExpression = expression.Body as UnaryExpression;
            if (unaryExpression == null)
                return expression.Body as MemberExpression;
            return unaryExpression.Operand as MemberExpression;
        }
    }
}
获取lambd表达式字段名
技术分享
using System.Collections.Generic;
using System.Linq;

namespace YZY.Domain
{
    public class PropertyValueHelper
    {
        /// <summary>
        /// 改变实例的指定属性值为默认值,返回改变后的实例
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="obj"></param>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        public static T GetChangeProValueToDefault<T>(T obj,List<string> propertyNames)
        {
            foreach (var item in propertyNames)
            {
                var pro = obj.GetType().GetProperties().Where(o => o.Name == item).FirstOrDefault();
                pro.SetValue(obj, default(object));
                
            }
            return obj;
        }
    }
}
修改属性值

为了最小化类型参数,我搞了必须的两个属性,以方便服务层中传相应的数据仓储。添加绑定时返回关联实例类,用IQueryable<T>来代替T类型的数据仓储,因此脱离数据库操作。方便测试

技术分享
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;

namespace YZY.Domain.Tests
{
    [TestClass()]
    public class RBACManageTests
    {
        public class UserRbac : RBACManage<User, Guid> { }
        public class RoleRbac : RBACManage<Role, Guid> { }
        void Initia()
        {
            for (int i = 0; i < 20; i++)
            {
                User u = new User { Name = "u" + i, Telephone = "tel" + i };
                uList.Add(u);
            }
            for (int i = 0; i < 5; i++)
            {
                Role r = new Role { Name = "r" + i };
                rList.Add(r);
            }
            rbacR = new RoleRbac();
            rbacR.RelevanceQuery = reList.AsQueryable();
            rbacR.HideFiledQuery = hList.AsQueryable();

            rbacU = new UserRbac();
            rbacU.HideFiledQuery = rbacR.HideFiledQuery;
            rbacU.RelevanceQuery = rbacR.RelevanceQuery;
        }
        private List<Relevance> reList = new List<Relevance>();
        private List<User> uList = new List<User>();
        private List<Role> rList = new List<Role>();
        private List<HideFiled> hList = new List<HideFiled>();

        private UserRbac rbacU;
        private RoleRbac rbacR;
        private Relevance rel = new Relevance();
        private HideFiled hide = new HideFiled();
        [TestMethod()]
        public void GetBindRelevanceTest()
        {
            Initia();
            var r0 = rList.Where(o => o.Name == "r0").First();
            //r0绑定一个u0
            var rel = rbacR.GetBindRelevance(r0, new Relevance(), uList.Where(o => o.Name == "u0").First(), PermissionType.User);
            Assert.AreEqual(rel.PermissionType.ToString(), "User");
            Assert.AreEqual(rel.EntityId, r0.Id);
            Assert.AreEqual(rel.EntityType, "Role");
        }

        [TestMethod()]
        public void GetRegisteHideFiledTest()
        {
            Initia();
            var u0 = uList.Where(o => o.Name == "u0").First();
            //U0注册name字段隐藏
            var hide = rbacU.GetRegisteHideFiled(u0, o => o.Name, new HideFiled());
            Assert.AreEqual(hide.EntityId, u0.Id);
            Assert.AreEqual(hide.HidePropertyName, "Name");
            Assert.AreEqual(hide.EntityType, "User");
        }

        [TestMethod()]
        public void GetBindHideFiledToOtherEntityTest()
        {
            Initia();
            var u0 = uList.Where(o => o.Name == "u0").First();
            //U0注册name字段隐藏
            hList.Add(rbacU.GetRegisteHideFiled(u0, o => o.Name, new HideFiled()) as HideFiled);
            rbacU.HideFiledQuery = hList.AsQueryable();

            //交U0的name字段对r0,r1隐藏
            var rs = rList.Where(o => o.Name == "r0" || o.Name == "r1").ToList();
            reList = new List<Relevance>();
            rs.ForEach(o => reList.Add(rbacU.GetBindHideFiledToOtherEntityRelevance(u0, new Relevance(), o, e => e.Name) as Relevance));
            rbacU.RelevanceQuery = reList.AsQueryable();

            var r0 = rList.Where(o => o.Name == "r0").First();
            var r1 = rList.Where(o => o.Name == "r1").First();

            //判断关联的个数,等于将两个角色同时绑定了一个用户的隐藏字段权限
       

以上是关于尝试封装适用于权限管理的通用API的主要内容,如果未能解决你的问题,请参考以下文章

干货 | 微服务架构下 Spring Cloud OAuth2 通用权限管理系统

防火墙中的入站规则仅适用于管理员权限

后台系统资源/数据权限系统设计

Vue + Element UI 实现权限管理系统 前端篇:工具模块封装

通用权限管理设计

通用的管理系统权限设计