咱們的業務系統使用了一段時間後,用戶的角色類型愈來愈多,這時候不一樣類型的用戶可使用不一樣功能,看見不一樣數據的需求就變得愈來愈迫切。 如何設計一個可擴展,且易於接入的權限系統.就顯得至關重要了。結合以前我實現的的權限系統,今天就來和你們探討一下我對權限系統的理解。前端
這篇文章會從權限系統業務設計,技術架構,關鍵代碼幾個方面,詳細的闡述權限系統的實現。java
權限系統是一個系統的基礎功能,可是做爲創業公司,秉承着快比完美更重要原則,老系統的權限系統都是硬編碼在代碼或者寫在到配置文件中的。隨着業務的發展,如此簡陋的權限系統就顯得捉襟見肘了。開發一套新的,強大的權限系統就提上了日程。web
這裏有兩個重點:算法
權限系統須要支持功能權限和數據權限。spring
所謂功能權限,就是指,擁有某種角色的用戶,只能看到某些功能,並使用它。實現功能權限就簡化爲:sql
所謂數據權限是指,數據是隔離的,用戶能看到的數據,是通過控制的,用戶只能看到擁有權限的某些數據。shell
好比,某個地區的 leader 能夠查看並操做這個地區的全部員工負責的訂單數據,可是員工就只能操做和查看本身負責的的訂單數據。編程
對於數據權限,咱們須要考慮的問題就抽象爲,後端
通過上面的分析,咱們能夠抽象出如下幾個實體:api
咱們知道,對於一某個功能來講,它是由若干的前端元素和後端 API 組成的。
好比「合同審覈」 這個功能就包括了,「查看按鈕」、「審覈按鈕」 等前端元素。
涉及的 api 就可能包含了 contract
的 get
和 patch
兩個 Restful 風格的接口。
抽象出來就是:在權限系統中若干前端元素和後端 API 組成了一個功能。
具體的關係,就是以下圖:
具體每一個系統的數據權限的實現有所不一樣,咱們這裏實現的數據權限是依賴於公司的組織架構實現的,全部涉及到的實體以下:
這裏須要說明一下,要接入數據權限,首先須要梳理數據的歸屬問題,數據歸屬於誰?或者準確的來講,數據屬於哪一個數據擁有者,這個數據擁有者屬於哪一個部門。經過這個關聯關係咱們就能夠明確,這個數據屬於哪一個部門。
對於數據的使用用戶,來講,就須要查詢,這個用戶能夠查看某個模塊的某個部門的數據。
這裏須要說明的是,不一樣的系統的數據權限須要具體分析,咱們系統的數據權限是創建在公司的組織架構上的。
本質就是:
具體的關係圖以下:
注意,實際上用戶和數據擁有者都是同一個實體 User 表示,只是爲了表述方便進行了區分。
能夠看出來,咱們的功能和組織架構都是典型的樹形結構。
咱們最多見的場景以下
抽象之後就是查詢樹的某個節點,和他的全部子節點。
爲了便於查詢,咱們能夠增長兩個冗餘字段,一個是 parent_id
,還有一個是 path
。
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 查詢是能夠走索引的。提高查詢效率。
咱們知道 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 接口。這樣咱們就能夠遍歷處理項目的接口數據。
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 實現方法攔截,很天然的就像到了使用攔截器來實現。考慮到咱們這個權限的組件是一個通用組件,因此就能夠寫一個抽象類,暴露出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);
}
}
複製代碼
經過上面的代碼能夠看出來,功能權限的實現,基本作到了沒有侵入代碼。對於數據權限的實現的原則仍是儘可能少的減小代碼的入侵。
咱們默認代碼使用 Java 經典的 Controller、Service、Dao 三層架構。 主要使用的技術 Spring Aop、Jpa 的 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
框架的高級特性,保證的代碼的靈活,以及橫向擴展的能力。
看完文章若是你發現有疑問,或者更好的實現方法,歡迎留言與我討論。