最近一直在重構系統,看到我們原來的代碼里,對於數據許可權的實現居然是在查詢語句里寫死的。 正感慨這祖傳代碼怎麼這麼坑,領導就讓我重新設計許可權模塊。這.... 好吧,反正都在重構代碼,直接推翻重來也不算填坑。 先開始梳理需求,所謂“數據許可權”,即經過普通的菜單、按鈕許可權後,對用戶能獲取到的數據再進行一次 ...
最近一直在重構系統,看到我們原來的代碼里,對於數據許可權的實現居然是在查詢語句里寫死的。
正感慨這祖傳代碼怎麼這麼坑,領導就讓我重新設計許可權模塊。這....
好吧,反正都在重構代碼,直接推翻重來也不算填坑。
先開始梳理需求,所謂“數據許可權”,即經過普通的菜單、按鈕許可權後,對用戶能獲取到的數據再進行一次許可權校驗。只顯示用戶有許可權訪問的數據。
經過一番思考我總結出了這個功能的幾個要點:
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); }
到這裡代碼其實已經寫完了。
感興趣的朋友們可以想想如果要添加一個基於用戶校驗要怎麼寫代碼。
如果有更好的寫法歡迎各位大神交流討論。