我們的業務系統使用了一段時間後,使用者的角色類型越來越多,這時候不同類型的使用者可以使用不同功能,看見不同資料的需求就變得越來越迫切。
如何設計一個可擴充,且易于接入的權限系統.就顯得相當重要了。結合之前我實作的的權限系統,今天就來和大家探讨一下我對權限系統的了解。
這篇文章會從權限系統業務設計,技術架構,關鍵代碼幾個方面,詳細的闡述權限系統的實作。
背景
權限系統是一個系統的基礎功能,但是作為創業公司,秉承着快比完美更重要原則,老系統的權限系統都是寫死在代碼或者寫在到配置檔案中的。随着業務的發展,如此簡陋的權限系統就顯得捉襟見肘了。開發一套新的,強大的權限系統就提上了日程。
這裡有兩個重點:
- 業務系統已經運作一段時間積累了可觀的代碼和接口了,新的權限系統權在設計之初的一個要求就是,盡量減少權限系統對原有業務代碼的入侵。(為了達成這個目的,我們會大量的使用 spring、springboot、jpa 以及 hibernate 的進階特性)
- 系統要易于使用,可以由業務方自行進行配置。
需求
權限系統需要支援功能權限和資料權限。
功能權限
所謂功能權限,就是指,擁有某種角色的使用者,隻能看到某些功能,并使用它。實作功能權限就簡化為:
- 頁面元素如何根據不同使用者進行渲染
- API 的通路權限如何根據不同的使用者進行管理
資料權限
所謂資料權限是指,資料是隔離的,使用者能看到的資料,是經過控制的,使用者隻能看到擁有權限的某些資料。
比如,某個地區的 leader 可以檢視并操作這個地區的所有員工負責的訂單資料,但是員工就隻能操作和檢視自己負責的的訂單資料。
對于資料權限,我們需要考慮的問題就抽象為,
- 資料的歸屬問題:資料産生以後歸屬于誰?
- 确定了資料的歸屬,根據某些配置,就能确定誰可以檢視歸屬于誰的資料。
業務設計
經過上面的分析,我們可以抽象出以下幾個實體:
功能權限
- 使用者
- 角色
- 功能
- 頁面元素
- API 資訊
我們知道,對于一某個功能來說,它是由若幹的前端元素和後端 API 組成的。
比如“合同稽核” 這個功能就包括了,“檢視按鈕”、“稽核按鈕” 等前端元素。
涉及的 api 就可能包含了
contract
的
get
和
patch
兩個 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).
這要的好處的就是通過
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,基本的實作思路如下圖:
date permission
基本的思路如下:
- 使用者登入以後,擷取使用者的資料權限相關資訊。
- 把相關資訊權限系統放入 ThreadLocal 中。
- 在 Dao 層中,從 ThreadLocal 中擷取權限相關的權限資料。
- 在 filter 中填充權限相關資料。
- 從 Hibernate 上下文中取出 Session。
- 在 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
架構的進階特性,保證的代碼的靈活,以及橫向擴充的能力。
看完文章如果你發現有疑問,或者更好的實作方法,歡迎留言與我讨論。