如何利用 Spring Hibernate 高級特性設計實現一個權限系統

keepout

咱們的業務系統使用了一段時間後,用戶的角色類型愈來愈多,這時候不一樣類型的用戶可使用不一樣功能,看見不一樣數據的需求就變得愈來愈迫切。 如何設計一個可擴展,且易於接入的權限系統.就顯得至關重要了。結合以前我實現的的權限系統,今天就來和你們探討一下我對權限系統的理解。前端

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

背景

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

這裏有兩個重點:算法

  • 業務系統已經運行一段時間積累了可觀的代碼和接口了,新的權限系統權在設計之初的一個要求就是,儘可能減小權限系統對原有業務代碼的入侵。(爲了達成這個目的,咱們會大量的使用 spring、springboot、jpa 以及 hibernate 的高級特性)
  • 系統要易於使用,能夠由業務方自行進行配置。

需求

權限系統須要支持功能權限和數據權限。spring

功能權限

所謂功能權限,就是指,擁有某種角色的用戶,只能看到某些功能,並使用它。實現功能權限就簡化爲:sql

  • 頁面元素如何根據不一樣用戶進行渲染
  • API 的訪問權限如何根據不一樣的用戶進行管理

數據權限

所謂數據權限是指,數據是隔離的,用戶能看到的數據,是通過控制的,用戶只能看到擁有權限的某些數據。shell

好比,某個地區的 leader 能夠查看並操做這個地區的全部員工負責的訂單數據,可是員工就只能操做和查看本身負責的的訂單數據。編程

對於數據權限,咱們須要考慮的問題就抽象爲,後端

  1. 數據的歸屬問題:數據產生之後歸屬於誰?
  2. 肯定了數據的歸屬,根據某些配置,就能肯定誰能夠查看歸屬於誰的數據。

業務設計

通過上面的分析,咱們能夠抽象出如下幾個實體:api

功能權限

  • 用戶
  • 角色
  • 功能
  • 頁面元素
  • 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框架的高級特性,保證的代碼的靈活,以及橫向擴展的能力。

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

原文地址

相關文章
相關標籤/搜索