天天看點

如何利用 Spring Hibernate 進階特性設計實作一個權限系統

我們的業務系統使用了一段時間後,使用者的角色類型越來越多,這時候不同類型的使用者可以使用不同功能,看見不同資料的需求就變得越來越迫切。

如何設計一個可擴充,且易于接入的權限系統.就顯得相當重要了。結合之前我實作的的權限系統,今天就來和大家探讨一下我對權限系統的了解。

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

背景

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

這裡有兩個重點:

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

需求

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

功能權限

所謂功能權限,就是指,擁有某種角色的使用者,隻能看到某些功能,并使用它。實作功能權限就簡化為:

  • 頁面元素如何根據不同使用者進行渲染
  • API 的通路權限如何根據不同的使用者進行管理

資料權限

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

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

對于資料權限,我們需要考慮的問題就抽象為,

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

業務設計

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

功能權限

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

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

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

涉及的 api 就可能包含了

contract

get

patch

兩個 Restful 風格的接口。

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

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

如何利用 Spring Hibernate 進階特性設計實作一個權限系統

permission-er

資料權限

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

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

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

對于資料的使用使用者,來說,就需要查詢,這個使用者可以檢視某個子產品的某個部門的資料。

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

本質就是:

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

具體的關系圖如下:

如何利用 Spring Hibernate 進階特性設計實作一個權限系統

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).

這要的好處的就是通過

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,基本的實作思路如下圖:

如何利用 Spring Hibernate 進階特性設計實作一個權限系統

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 就能夠接入權限系統。

總結

權限系統随着業務的發展,是從可以沒有逐漸變成為非常重要的子產品。往往需要接入權限系統的時候,系統已經成熟的運作了一段時間了。大量的接口,負責的業務,為權限系統的接入提高了難度。同時權限系統又是看似通用,但是定制的點又不少的系統。

設計套權限系統的初衷就是,不需要大量修改代碼,業務方就可友善簡單的接入。

具體實作代碼的時候,我們充分利用了面向切面的程式設計思想。同時大量的使用了

Spring

Hibrenate

架構的進階特性,保證的代碼的靈活,以及橫向擴充的能力。

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