我們的業務系統使用了一段時間後,使用者的角色型別越來越多,這時候不同型別的使用者可以使用不同功能,看見不同資料的需求就變得越來越迫切。 如何設計一個可擴充套件,且易於接入的許可權系統.就顯得相當重要了。結合之前我實現的的許可權系統,今天就來和大家探討一下我對許可權系統的理解。
這篇文章會從許可權系統業務設計,技術架構,關鍵程式碼幾個方面,詳細的闡述許可權系統的實現。
背景
許可權系統是一個系統的基礎功能,但是作為創業公司,秉承著快比完美更重要原則,老系統的許可權系統都是硬編碼在程式碼或者寫在到配置檔案中的。隨著業務的發展,如此簡陋的許可權系統就顯得捉襟見肘了。開發一套新的,強大的許可權系統就提上了日程。
這裡有兩個重點:
- 業務系統已經執行一段時間積累了可觀的程式碼和介面了,新的許可權系統權在設計之初的一個要求就是,儘量減少許可權系統對原有業務程式碼的入侵。(為了達成這個目的,我們會大量的使用 spring、springboot、jpa 以及 hibernate 的高階特性)
- 系統要易於使用,可以由業務方自行進行配置。
需求
許可權系統需要支援功能許可權和資料許可權。
功能許可權
所謂功能許可權,就是指,擁有某種角色的使用者,只能看到某些功能,並使用它。實現功能許可權就簡化為:
- 頁面元素如何根據不同使用者進行渲染
- API 的訪問許可權如何根據不同的使用者進行管理
資料許可權
所謂資料許可權是指,資料是隔離的,使用者能看到的資料,是經過控制的,使用者只能看到擁有許可權的某些資料。
比如,某個地區的 leader 可以檢視並操作這個地區的所有員工負責的訂單資料,但是員工就只能操作和檢視自己負責的的訂單資料。
對於資料許可權,我們需要考慮的問題就抽象為,
- 資料的歸屬問題:資料產生以後歸屬於誰?
- 確定了資料的歸屬,根據某些配置,就能確定誰可以檢視歸屬於誰的資料。
業務設計
經過上面的分析,我們可以抽象出以下幾個實體:
功能許可權
- 使用者
- 角色
- 功能
- 頁面元素
- API 資訊
我們知道,對於一某個功能來說,它是由若干的前端元素和後端 API 組成的。
比如“合同稽核” 這個功能就包括了,“檢視按鈕”、“稽核按鈕” 等前端元素。
涉及的 api 就可能包含了 contract
的 get
和 patch
兩個 Restful 風格的介面。
抽象出來就是:在許可權系統中若干前端元素和後端 API 組成了一個功能。
具體的關係,就是如下圖:
資料許可權
具體每個系統的資料許可權的實現有所不同,我們這裡實現的資料許可權是依賴於公司的組織架構實現的,所有涉及到的實體如下:
- 使用者
- 資料許可權關係
- 部門
- 資料擁有者
- 具體資料(訂單,合同)
這裡需要說明一下,要接入資料許可權,首先需要梳理資料的歸屬問題,資料歸屬於誰?或者準確的來說,資料屬於哪個資料擁有者,這個資料擁有者屬於哪個部門。通過這個關聯關係我們就可以明確,這個資料屬於哪個部門。
對於資料的使用使用者,來說,就需要查詢,這個使用者可以檢視某個模組的某個部門的資料。
這裡需要說明的是,不同的系統的資料許可權需要具體分析,我們系統的資料許可權是建立在公司的組織架構上的。
本質就是:
- 資料歸屬於某個資料擁有者
- 使用者能夠看到該資料擁有者的資料
具體的關係圖如下:
注意,實際上使用者和資料擁有者都是同一個實體 User 表示,只是為了表述方便進行了區分。
實現的技術難點
Mysql 中樹的儲存
可以看出來,我們的功能和組織架構都是典型的樹形結構。
我們最常見的場景如下
- 查詢某個功能,及其所有子功能。
- 查詢某個部門,及其所有子部門的所屬員工。
抽象以後就是查詢樹的某個節點,和他的所有子節點。
為了便於查詢,我們可以增加兩個冗餘欄位,一個是 parent_id
,還有一個是 path
。
- parent_id 很好理解,就是父節點的 id;
- path 指的是,這個節點,路徑上的 id 的。使用'.'進行分隔的一個字串。 比如
A
/ \
B C
/\ /\
D E F G
/\
H I
複製程式碼
對於 D 的 path 就是 (A.id).(B.id).
這要的好處的就是通過 sql
的 like
的語句就能快速的查詢出某個節點的子節點。
比如要獲取節點 C 的所有子節點:
Select * from user where path like (A.id).(C.id).%
複製程式碼
一次查詢可以獲取所有子節點,是一種查詢友好的設計。如果需要我們可以為 path
欄位增加索引,根據索引的左值定律,這樣的 like 查詢是可以走索引的。提升查詢效率。
快速的自動的獲取 API 資訊
我們知道 Spirng mvc
在啟動的時候會掃描被 @RequestMapping
註解標記的方法,並把資料放在 RequestMappingHandlerMapping
中。所以我們可以這樣:
@Componet
public class ApiScanSerivce{
@Autoired
private RequestMappingHandlerMapping requestMapping;
@PostConstruct
public void update(){
Map<RequestMappingInfo,HandlerMethed> handlerMethods = requestMapping.getHandlerMethods();
for(Map.Entry RequestMappinInfo,HandlerMethod) entry: handlerMethods.entrySet(){
// 處理 API 上傳的相關邏輯
updateApiInfo();
}
}
}
複製程式碼
獲取專案的所有 http 介面。這樣我們就可以遍歷處理專案的介面資料。
描述一個 API
public class ApiInfo{
private Long id;
private String uri; // api 的 uri
private String method; //請求的 method:eg: get、 post、 patch。
private String project; // 這組 api 屬於哪一個 web 工程。
private String signature; //方法的簽名
private Intger status; // api 狀態
private Intger whiteList; // 是否是白名單 api 如果是就不需過濾
}
複製程式碼
其中方法的簽名生成的演算法虛擬碼:
signature = className + "#" + methodName +"(" + parameterTypeList+")"
複製程式碼
使用者的許可權資料
首先我們定義的使用者許可權資料如下:
@Data
@ToString
public class UserPermisson{
//使用者可以看到的前端元素的列表
private List<Long> pageElementIdList;
//使用者可以使用的 API 列表
private List<String> apiSignatureList;
//使用者不同模組的資料許可權 的 map。map 的 key 是模組名稱,value 是這個能夠看到資料屬於那些使用者的列表
private Map<String,List<Long>> dataAccessMap;
}
複製程式碼
利用 Spring 特性實現功能許可權
對於如何使用 Spring 實現方法攔截,很自然的就像到了使用攔截器來實現。考慮到我們這個許可權的元件是一個通用元件,所以就可以寫一個抽象類,暴露出getUid(HttpServletRequest requset)
使用者獲取使用系統的 userId
,以及 onPermission(String msg)
留給業務方自己實現,沒有許可權以後的動作。
public abstract class PermissonAbstractInterceptor extends HandlerInterceptorAdapter{
protected abstarct long getUid(HttpServletRequest requset);
protected abstract onPermession(String str) throws Exception;
@Override
public boolean preHandler(HttpServletRequest request,HttoServletResponse respponse,Object handler) throws Excption{
// 獲取使用者的 uid
long uid = getUid(request);
// 根據使用者 獲取使用者相關的 許可權物件
UserPermisson userPermission = getUserPermissonByUid(uid);
if(inandler instanceof HanderMethod){
//獲取請求方的簽名
String methodSignerture = getMethodSignerture(handler);
if(!userPermisson.getApiSignatureList().contains(methodSignerture)){
onPermession("該使用者沒有許可權");
}
}
}
}
複製程式碼
以上的程式碼只是提供一個思路。不是真實的程式碼實現。
所以接入方就只需要繼承這個抽象方法,並實現對應的方法,如果你使用的是 Springboot 的,只需要把實現的攔截器註冊到攔截器裡面就可以使用了:
@Configuration
public class MyWebAppConfigurer extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(permissionInterceptor);
super.addInterceptors(registry);
}
}
複製程式碼
利用 Hibrenate 特性實現資料許可權
通過上面的程式碼可以看出來,功能許可權的實現,基本做到了沒有侵入程式碼。對於資料許可權的實現的原則還是儘量少的減少程式碼的入侵。
我們預設程式碼使用 Java 經典的 Controller、Service、Dao 三層架構。 主要使用的技術 Spring Aop、Jpa 的 filter,基本的實現思路如下圖:
基本的思路如下:
- 使用者登入以後,獲取使用者的資料許可權相關資訊。
- 把相關資訊許可權系統放入 ThreadLocal 中。
- 在 Dao 層中,從 ThreadLocal 中獲取許可權相關的許可權資料。
- 在 filter 中填充許可權相關資料。
- 從 Hibernate 上下文中取出 Session。
- 在 Session 上新增相關 filter。
通過圖片我們可以看出,我們基本不需要對 Controller、Service、Dao 進行修改,只需要按需實現對應模組的 filter。
看到這裡你可能覺得"嚯~~",還有這種操作?我們就看看程式碼是怎麼具體實現的吧。
-
首先需要在 Entity 上寫一個 Filter,假設我們寫的是訂單模組。
@Entity @Table(name = "order") @Data @ToString @FilterDef(name = "orderOwnerFilter", parameters = {@ParamDef name= "ownerIds",type = "long"}) @Filters({@Filter name= "orderOwnerFiler", condition = "ownder in (:ownerIds)"}) public class order{ private Long id; private Long ownerId; //其他引數省略 } 複製程式碼
-
寫個註解
@Retention(RetentinPolicy.RUNTIME) @Taget(ElementType.METHOD) public @interface OrderFilter{ } 複製程式碼
-
編寫一個切面用於處理 Session、datePermission、和 Filter
@Component @Aspect public class OrderFilterAdvice{ @PersistenceContext private EntityManager entityManager; @Around("annotation(OrderFilter)") pblict Object doProcess (ProceedingJoinPoint joinPonit) throws ThrowableP{ try{ //從上下文裡面獲取 owerId,這個 Id 在 web 中就已經存好了 List<Long> ownerIds = getListFromThreadLocal(); //獲取查詢中的 session Session session = entityManager.unwrap(Session.class); // 在 session 中加入 filter Filter filter = unwrap.enableFilter("orderOwnerFilter"); // filter 中加入資料 filter.setParameterList("ownerIds",ownerIds) //執行 被攔截的方法 return join.proceed(); }catch(Throwable e){ log.error(); }finally{ // 最後 disable filter entityManager.unwrap(Session.class).disbaleFilter("orderOwnerFilter"); } } } 複製程式碼
這個攔截器,攔截被打了
@OrderFilter
的方法。
易於接入
為了方便接入專案,我們可以將涉及到的整套程式碼封裝為一個 springboot-starter
這樣使用者只需要引入對應的 starter 就能夠接入許可權系統。
總結
許可權系統隨著業務的發展,是從可以沒有逐漸變成為非常重要的模組。往往需要接入許可權系統的時候,系統已經成熟的執行了一段時間了。大量的介面,負責的業務,為許可權系統的接入提高了難度。同時許可權系統又是看似通用,但是定製的點又不少的系統。
設計套許可權系統的初衷就是,不需要大量修改程式碼,業務方就可方便簡單的接入。
具體實現程式碼的時候,我們充分利用了面向切面的程式設計思想。同時大量的使用了 Spring
、Hibrenate
框架的高階特性,保證的程式碼的靈活,以及橫向擴充套件的能力。
看完文章如果你發現有疑問,或者更好的實現方法,歡迎留言與我討論。