如何利用 Spring Hibernate 高階特性設計實現一個許可權系統

犀利豆發表於2019-05-13

keepout

我們的業務系統使用了一段時間後,使用者的角色型別越來越多,這時候不同型別的使用者可以使用不同功能,看見不同資料的需求就變得越來越迫切。 如何設計一個可擴充套件,且易於接入的許可權系統.就顯得相當重要了。結合之前我實現的的許可權系統,今天就來和大家探討一下我對許可權系統的理解。

這篇文章會從許可權系統業務設計,技術架構,關鍵程式碼幾個方面,詳細的闡述許可權系統的實現。

背景

許可權系統是一個系統的基礎功能,但是作為創業公司,秉承著快比完美更重要原則,老系統的許可權系統都是硬編碼在程式碼或者寫在到配置檔案中的。隨著業務的發展,如此簡陋的許可權系統就顯得捉襟見肘了。開發一套新的,強大的許可權系統就提上了日程。

這裡有兩個重點:

  • 業務系統已經執行一段時間積累了可觀的程式碼和介面了,新的許可權系統權在設計之初的一個要求就是,儘量減少許可權系統對原有業務程式碼的入侵。(為了達成這個目的,我們會大量的使用 spring、springboot、jpa 以及 hibernate 的高階特性)
  • 系統要易於使用,可以由業務方自行進行配置。

需求

許可權系統需要支援功能許可權和資料許可權。

功能許可權

所謂功能許可權,就是指,擁有某種角色的使用者,只能看到某些功能,並使用它。實現功能許可權就簡化為:

  • 頁面元素如何根據不同使用者進行渲染
  • API 的訪問許可權如何根據不同的使用者進行管理

資料許可權

所謂資料許可權是指,資料是隔離的,使用者能看到的資料,是經過控制的,使用者只能看到擁有許可權的某些資料。

比如,某個地區的 leader 可以檢視並操作這個地區的所有員工負責的訂單資料,但是員工就只能操作和檢視自己負責的的訂單資料。

對於資料許可權,我們需要考慮的問題就抽象為,

  1. 資料的歸屬問題:資料產生以後歸屬於誰?
  2. 確定了資料的歸屬,根據某些配置,就能確定誰可以檢視歸屬於誰的資料。

業務設計

經過上面的分析,我們可以抽象出以下幾個實體:

功能許可權

  • 使用者
  • 角色
  • 功能
  • 頁面元素
  • API 資訊

我們知道,對於一某個功能來說,它是由若干的前端元素和後端 API 組成的。

比如“合同稽核” 這個功能就包括了,“檢視按鈕”、“稽核按鈕” 等前端元素。

涉及的 api 就可能包含了 contractgetpatch 兩個 Restful 風格的介面。

抽象出來就是:在許可權系統中若干前端元素和後端 API 組成了一個功能。

具體的關係,就是如下圖:

permission-er

資料許可權

具體每個系統的資料許可權的實現有所不同,我們這裡實現的資料許可權是依賴於公司的組織架構實現的,所有涉及到的實體如下:

  • 使用者
  • 資料許可權關係
  • 部門
  • 資料擁有者
  • 具體資料(訂單,合同)

這裡需要說明一下,要接入資料許可權,首先需要梳理資料的歸屬問題,資料歸屬於誰?或者準確的來說,資料屬於哪個資料擁有者,這個資料擁有者屬於哪個部門。通過這個關聯關係我們就可以明確,這個資料屬於哪個部門。

對於資料的使用使用者,來說,就需要查詢,這個使用者可以檢視某個模組的某個部門的資料。

這裡需要說明的是,不同的系統的資料許可權需要具體分析,我們系統的資料許可權是建立在公司的組織架構上的。

本質就是:

  • 資料歸屬於某個資料擁有者
  • 使用者能夠看到該資料擁有者的資料

具體的關係圖如下:

date-permission

注意,實際上使用者和資料擁有者都是同一個實體 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). 這要的好處的就是通過 sqllike 的語句就能快速的查詢出某個節點的子節點。

比如要獲取節點 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,基本的實現思路如下圖:

date permission

基本的思路如下:

  1. 使用者登入以後,獲取使用者的資料許可權相關資訊。
  2. 把相關資訊許可權系統放入 ThreadLocal 中。
  3. 在 Dao 層中,從 ThreadLocal 中獲取許可權相關的許可權資料。
  4. 在 filter 中填充許可權相關資料。
  5. 從 Hibernate 上下文中取出 Session。
  6. 在 Session 上新增相關 filter。

通過圖片我們可以看出,我們基本不需要對 Controller、Service、Dao 進行修改,只需要按需實現對應模組的 filter。

看到這裡你可能覺得"嚯~~",還有這種操作?我們就看看程式碼是怎麼具體實現的吧。

  1. 首先需要在 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;
        //其他引數省略
    }
    複製程式碼
  2. 寫個註解

    @Retention(RetentinPolicy.RUNTIME)
    @Taget(ElementType.METHOD)
    public @interface OrderFilter{
    }
    複製程式碼
  3. 編寫一個切面用於處理 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 就能夠接入許可權系統。

總結

許可權系統隨著業務的發展,是從可以沒有逐漸變成為非常重要的模組。往往需要接入許可權系統的時候,系統已經成熟的執行了一段時間了。大量的介面,負責的業務,為許可權系統的接入提高了難度。同時許可權系統又是看似通用,但是定製的點又不少的系統。

設計套許可權系統的初衷就是,不需要大量修改程式碼,業務方就可方便簡單的接入。 具體實現程式碼的時候,我們充分利用了面向切面的程式設計思想。同時大量的使用了 SpringHibrenate框架的高階特性,保證的程式碼的靈活,以及橫向擴充套件的能力。

看完文章如果你發現有疑問,或者更好的實現方法,歡迎留言與我討論。

原文地址

相關文章