最近一直在重构系统,看到我们原来的代码里,对于数据权限的实现居然是在查询语句里写死的。
正感慨这祖传代码怎么这么坑,领导就让我重新设计权限模块。这....
好吧,反正都在重构代码,直接推翻重来也不算填坑。
先开始梳理需求,所谓“数据权限”,即经过普通的菜单、按钮权限后,对用户能获取到的数据再进行一次权限校验。只显示用户有权限访问的数据。
经过一番思考我总结出了这个功能的几个要点:
1.权限针对用户能看到的数据(看不到也就无从操作了),这些数据的来源都是列表查询。
2.这些列表查询都含有需要校验权限的字段。
3.同一种需要校验权限的字段存在不同的权限校验方式。
分析完需求,我马上想到了设计模式里的装饰器模式。
什么是装饰器模式?
装饰器模式就是当你要给一个对象“穿衣服”时,把衣服封装起来。穿衣服的类不需要关心“衣服”的实现,只管穿。
这样一来就可以在不修改类的情况下随意增加或者重新设计“衣服”种类。
代入我们的场景,装饰器模式的实现就变成了这样:
我的query需要经过权限校验筛选出更少的数据,我们把权限校验筛选的操作封装起来,再设计一个校验引擎之类的方法来给query选择该如何校验。这样一来原来写好的query不需要改变。
query不需要改变是最关键的。整个系统那么多列表查询,如果权限改变了就全都要修改(如原来的那种写法),那简直是灾难。
简单了解了什么是装饰器模式之后,我们再做一些准备工作就可以开始写装饰器了。
准备工作就是给我们的query写一个接口,配合泛型使用,让装饰器知道传进来的query一定含有需要校验权限的字段。
public interface IAuthorityEntity { /// <summary> /// 管理部门 /// </summary> /// <returns></returns>string MaintainDept { get; set; } }
有了这个接口,我们现在可以开始动手写装饰器了,首先我们定义一个装饰器接口:
这里使用泛型约束规定传进来的query必须实现我们刚才定义的IAuthorityEntiy接口。
所有的装饰器都必须实现Filter方法,我们通过这个方法来将不满足权限的数据过滤掉。
public interface IAuthorityComponent<T> where T : class,IAuthorityEntity { IQueryable<T> Filter(IQueryable<T> query,string key); }
所有装饰器要实现这个接口,这样我们的校验引擎可以通过依赖注入的方式来获取不同的装饰器实现。
接下来我们先简单实现几个基于部门筛选的装饰器:
public abstract class AbstractDepartmentFilter<T> : IAuthorityComponent<T> where T : class,IAuthorityEntity { protected IOrganizationBLL organizationBLL; public virtual IQueryable<T> Filter(IQueryable<T> query, string departmentID) { query = query.Where(f => f.MaintainDept.Contains(departmentID)); return query; } } /// <summary> /// 本部门 /// </summary> public class DepartmentFilter<T> : AbstractDepartmentFilter<T> where T : class, IAuthorityEntity { } /// <summary> /// 指定部门 /// </summary> public class DesignatedDepartmentFilter<T> : AbstractDepartmentFilter<T> where T : class, IAuthorityEntity { public override IQueryable<T> Filter(IQueryable<T> query, string deptID) { if (!string.IsNullOrWhiteSpace(deptID)) { var keys = deptID.Split(','); query = query.Where(f => keys.Contains(f.MaintainDept)); return query; } return query.Where(f => false); } } /// <summary> /// 本部门及子部门 /// </summary> public class DepartmentAndChildDepartmentFilter<T> : AbstractDepartmentFilter<T> where T : class, IAuthorityEntity { ...... }
好了,有了这些装饰器,我们可以开始写引擎来装饰query了。
也是一样先来个接口和抽象类:
public interface IAuthority<T> where T : class, IAuthorityEntity { IQueryable<T> AuthorityFilter(IQueryable<T> query, EnumAccessScope accessScope, string key); }
public abstract class AbstractAuthority<T> : IAuthority<T> where T : class, IAuthorityEntity { public virtual IQueryable<T> AuthorityFilter(IQueryable<T> query, EnumAccessScope accessScope, string key) { return AuthorityCore(query, accessScope, key); } protected virtual IQueryable<T> AuthorityCore(IQueryable<T> query, EnumAccessScope accessScope, string key) { throw new ExecutionException("该方法未实现"); } }
接下来实现一个基于部门校验的引擎:
public class DepartmentAuthority<T> : AbstractAuthority<T> where T : class, IAuthorityEntity { IOrganizationBLL organizationBLL; public DepartmentAuthority(IOrganizationBLL organizationBLL) { this.organizationBLL = organizationBLL; } protected override IQueryable<T> AuthorityCore(IQueryable<T> query, EnumAccessScope accessScope, string key) { var deptQuery = query; switch (accessScope) { case EnumAccessScope.All: { break; } case EnumAccessScope.Department: { query = new DepartmentFilter<T>().Filter(deptQuery, key); break; } case EnumAccessScope.DepartmentAndChildDepartment: { query = new DepartmentAndChildDepartmentFilter<T>(organizationBLL).Filter(deptQuery, key); break; } case EnumAccessScope.DesignatedDepartment: { query = new DesignatedDepartmentFilter<T>().Filter(deptQuery, key); break; } default: { throw new Exception("权限读取错误"); } } return query; } }
如果以后新增了一个权限是需要用到不止一种判断(如指定部门+本部门),在case里多调一个或多个Filter即可实现“套餐”权限。
写完引擎之后,接下来就是在外面调用时选择适合自己的权限套餐了:
这里跟我们系统的业务代码相关性比较高,我就把一些逻辑省略了。
大体思路是注入合适版本的权限引擎,然后将query,权限和参数(如指定的部门ID)传入引擎。
protected IQueryable<T> Authority<T>(IQueryable<T> query, string controllerName) where T : class, IAuthorityEntity { var factoryKey = string.Empty; var accessScope = 0; //具体的权限获取和判断逻辑省略 //这里使用autofac来注入权限引擎 var factory = AutofacConfig.container.Resolve<AuthorityFactory<T>>(); var authority = factory.Classes[factoryKey]; return authority.AuthorityFilter(query, accessScope, key); }
到这里代码其实已经写完了。
感兴趣的朋友们可以想想如果要添加一个基于用户校验要怎么写代码。
如果有更好的写法欢迎各位大神交流讨论。