背景說明
近期寫代碼又開始重新接觸了一點控台應用,接觸到的項目年代久遠,所有的權限管理用起來感覺不是很得心應手。
於是想着自己能否從零設計一個,梳理一下思路,當然實際用不用也無所謂。
權限管理主要是為了安全,項目中的權限管理是全部放在前端控制的,感覺這一點非常不安全。
前端防君子,不防小人。

籠子.jpg
當然本次造輪子主要也是為了打造一款自己滿意的權限控制框架,所以設計採用 MVP 模式,採用漸進式的方式開發。
可以一起學習一下權限控制的設計和實現思路。
如果生產想直接使用,也推薦目前比較成熟的框架:
spring-security
Spring Security-01-Hello World
shiro
Shiro
權限設計的基礎知識
寫代碼之前,我們先學習一點權限設計的基礎知識。
權限設計發展至今,網上的資料還是比較豐富的,我們節選一些比較經典的內容。
用戶-權限 模型
對於權限管理,最簡單的想法應該是為每一個用戶,分配不同的權限。

輸入圖片說明
但是這種設計存在一個比較大的問題就是不夠靈活,當用戶較多時,比較難以維護。
RBAC0 模型
於是,很多人想到了在用戶和權限中間增加一個角色。
也是迄今為止迄最為普及的權限設計模型,基於角色的訪問控制(Role-Based Access Control)。

輸入圖片說明
這裡主要有三個概念:用戶,角色,權限。
用戶其實就是我們需要控制的主體。
角色就是一座橋樑,也是這個模型中最巧妙的地方。就像我們每一個人一樣,我們是父母的孩子,孩子的父母,公司的員工,我們在不同的時刻飾演着不同的角色。不同的角色,對應的不同的權限。
權限是一個比較寬泛的概念,對於控台而言,可能是菜單權限,資源權限,也包括對於數據的增刪改查的操作權限。
在數據庫表設計的時候,其實就是三張實體表:
user 用戶表
role 角色表
privilege 權限表
然後兩張映射表:
user_role 用戶-角色關係表
role_privilege 角色-權限關係表
以上是RBAC的核心設計及模型分析,此模型也叫做RBAC0,而基於核心概念之上,RBAC還提供了擴展模式。

輸入圖片說明
包括RBAC1,RBAC2,RBAC3模型。
下面介紹這三種類型
RBAC1 模型
這裡主要是引入了角色權限繼承(Hierarchical Role)的概念。

輸入圖片說明
這種設計可以給角色分組和分層,一定程度簡化了權限管理工作。
RBAC2模型
基於核心模型的基礎上,進行了角色的約束控制,RBAC2模型中添加了責任分離關係,其規定了權限被賦予角色時,或角色被賦予用戶時,以及當用戶在某一時刻激活一個角色時所應遵循的強制性規則。
不過實際工作中,這種模型用的也不多。此處不做展開。
RBAC3模型
功能最全的模型,也是各大公司最常用的一種模型。
我們平時見到的權限管理,一般是和公司的組織架構是一一對應的。
這就是,模型來源於生活。
用戶組
最常見的一個概念就是用戶組。
比如我們常用的 gitlab,加入一個項目組,然後就可以對相關的代碼倉庫進行操作。
就像我們在真正的工作中加入了一個項目組,是相同的道理。這裡的用戶一般是平級的。
公司中一般組織都是有上下級關係的,一般公司的組織架構圖如下:

輸入圖片說明
這種架構的好處就是便於權限的變更調整,也便於權限的控制。
這種關係我們在設計表的時候,就可以加一個 user_group 表,將某些用戶放在同一個組中。
職位
職位其實對應的是角色的概念,不同的職位權限也是不同的,即使在同一個項目中。
就像項目負責人和普通開發之間的區別。

輸入圖片說明
權限模型
最終的模型大概是下面的這個樣子:

輸入圖片說明
授權流程
給用戶賦予角色,這個是如何實現的呢?
手動授權
最常見的是手動設計。
項目初始化的時候,有一些基本的角色和管理員等信息。
然後管理員進行相關的權限配置。
審批授權
用戶可以申請對應的角色,然後通過審批流程,賦予其對應的角色。
比如我們有時候可以申請訪問某一個資源,實際上背後也是類似的道理。
其實到這裡,都只是一些基礎知識。
如果想設計好菜單,還有很多東西需要考慮,比如菜單的設計,頁面的設計,流程怎麼設計更加合理?可以自定義角色嗎?用戶可以自定義菜單嗎?
等等。不過這些暫時我們不做深入討論,我們本篇的重點是自己實現一個簡單版本的權限控制框架。
傳統 RBAC 的不足
當然上面的模型還是存在不足之處的。
比如我讀的一篇 paper 中所言:
儘管RBAC已經得到廣泛應用,但傳統的RBAC模型仍存在不足之處,主要有以下兩個方面:(1)訪問控制不能滿足實際應用的需要,Web應用系統需要更加細粒度的訪問控制。 (2)該模型僅僅定義了訪問控制的內部機制,並沒有提出簡單友好的訪問控制實現方式,而對於Web系統的用戶而言,友好直觀的用戶接口是系統必不可少的組成部分。
解決方案是設計了一個操作碼,然後通過字符串標識比如: 10000 可以代表可訪問菜單,沒有增刪改查權限。
當然這些都需要我們結合自己的業務去設計。
我們假定設計產品設計完成,數據庫也設計完成的情況下,如何在編碼時靈活的實現權限控制呢?
設計思路
最基本的思路就是類似於 shiro 的方式。
可以指定角色,或者權限來訪問固定的菜單。
設計目標
(1)當然我們希望可以更加靈活,可以具體到固定的一個方法,而不是拘泥於固定的某一個 Controller 請求。
(2)編寫方便,後期可以使用註解指定
(3)由簡到繁,初期只實現基於權限編碼的控制,暫時不考慮基於角色的控制。因為每一個登錄的用戶,都可以獲取到對應的角色和權限編碼。
整體流程
- 用戶頁面請求
- 獲取用戶登錄信息。分佈式系統可以基於 redis session 或者 JWT 等,獲取當前用戶的所有權限編碼
- 校驗用戶是否擁有請求的權限
- 返回對應的頁面結果
第一步-接口設計
核心接口
public interface IPrivilege {
/**
* 是否擁有權限
*
* {@code true} 擁有
* {@code false} 不擁有
*
* @param context 上下文
* @return 是否
* @since 0.0.1
*/
boolean hasPrivilege(final IPrivilegeContext context);
}
這裡就是一切的核心,我們關心的只是是否有權限,結果就是一個 boolean 值。
那麼 context 的內容是什麼呢?
public interface IPrivilegeContext {
/**
* 擁有的權限上下文
* @return 擁有
* @since 0.0.1
*/
IPrivilegeOwn own();
/**
* 需要的權限上下文
* @return 擁有
* @since 0.0.1
*/
IPrivilegeAcquire acquire();
}
這裡實際上有兩個接口,一個是擁有的權限,另一個是需要的權限。
權限信息
兩個權限的內容如下:
- 擁有的權限
public interface IPrivilegeOwn {
/**
* 擁有的權限編碼列表
* @return 編碼列表
* @since 0.0.1
*/
List<IPrivilegeInfo> ownPrivilege();
}
- 需要的權限
public interface IPrivilegeAcquire {
/**
* 需要的權限編碼列表
* @return 編碼列表
* @since 0.0.1
*/
List<IPrivilegeInfo> acquirePrivilege();
}
第二步-編程式實現
maven 引入
<dependency>
<group>com.github.houbb</group>
<artifact>privilege-core</artifact>
<version>${最新版本}</version>
</dependency>
自定義擁有的權限
實現 IPrivilegeOwn 接口即可。
可以查詢數據庫,文件等。
public class PrivilegeOwnOne implements IPrivilegeOwn {
@Override
public List<IPrivilegeInfo> ownPrivilege() {
IPrivilegeInfo info = PrivilegeInfo.newInstance().code("001");
return Collections.singletonList(info);
}
}
自定義需要的權限
public class PrivilegeAcquireTwo implements IPrivilegeAcquire {
@Override
public List<IPrivilegeInfo> acquirePrivilege() {
IPrivilegeInfo one = PrivilegeInfo.newInstance().code("001");
IPrivilegeInfo two = PrivilegeInfo.newInstance().code("002");
return Arrays.asList(one, two);
}
}
測試1
IPrivilegeOwn own = new PrivilegeOwnOne();
IPrivilegeAcquire acquire = new PrivilegeAcquireTwo();
boolean hasPrivilege = PrivilegeBs.newInstance()
.own(own)
.acquire(acquire)
.hasPrivilege();
Assert.assertFalse(hasPrivilege);
這裡我們得到的結果是沒有權限,因為默認的策略是指定的編碼全部擁有,才算通過。
內置策略
內置的策略,可以通過 Privileges 直接獲取。
策略 說明 all() 需要的權限編碼,全部都擁有,才認為擁有權限。(默認策略) any() 需要的權限編碼,擁有任何一個,則認為擁有權限。 allow() 白名單,直接返回擁有權限。 deny() 黑名單,直接返回不擁有權限。
測試2
我們指定擁有一個就可以通過
IPrivilegeOwn own = new PrivilegeOwnOne();
IPrivilegeAcquire acquire = new PrivilegeAcquireTwo();
IPrivilege privilege = Privileges.any();
boolean hasPrivilege = PrivilegeBs.newInstance()
.own(own)
.acquire(acquire)
.privilege(privilege)
.hasPrivilege();
Assert.assertTrue(hasPrivilege);
第三步-註解式編程
註解的必要性
當然上面已經實現了一個最基本的實現,但是實際使用肯定不能讓開發者手動指定。
那該多麻煩。
敘事我們設計可以基於註解的實現。
註解定義
@Inherited
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PrivilegeAcquire {
/**
* 權限編碼
* @return 編碼
* @since 0.0.4
*/
String[] code() default {};
}
這個註解可以放在方法上,指定訪問時需要的編碼。
maven 引入
我們一般開發,都是在 spring-mvc 項目中使用。
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>privilege-mvc</artifactId>
<version>${最新版本}</version>
</dependency>
自定義攔截器
我們定義一個屬於自己的攔截器,用來處理登錄用戶的權限。
@Component
public class MyPrivilegeInterceptor extends AbstractPrivilegeInterceptor {
@Override
protected void fillSubject(ISubject subject, HttpServletRequest request) {
String id = request.getParameter("id");
if("admin".equals(id)) {
subject.privileges("1001");
}
}
}
我們只為 admin 用戶設置權限編碼為 1001,其他用戶不做處理。
註冊攔截器
將我們的攔截器註冊一下,當訪問 /hello 這個地址的時候,會生效。當然也可以根據實際情況調整。
@Configuration
public class InterceptorConfig extends WebMvcConfigurerAdapter {
@Autowired
private MyPrivilegeInterceptor myPrivilegeInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myPrivilegeInterceptor)
.addPathPatterns("/hello");
}
}
業務代碼
Controller
我們通過註解 @PrivilegeAcquire 指定訪問當前方法時,需要權限編碼 1001
@Controller
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
@PrivilegeAcquire(code = "1001")
public String hello() {
return "hello";
}
}
Application
啟動類定義如下:
我們通過註解 @EnablePrivilege 啟動權限校驗。
@SpringBootApplication
@EnablePrivilege
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
測試
頁面訪問 http://localhost:8080/hello 報錯權限不足;
There was an unexpected error (type=Internal Server Error, status=500).
Has no privilege! Acquire: <[1001]>, Own: <[]>
頁面訪問 http://localhost:8080/hello?id=admin 正常訪問。
小結
到這裡一個最簡單的權限設計就完成了。
實際上設計的時候,為了適應各種場景,項目是非常多個模塊的:
(1)api 核心的接口和註解定義
(2)core 編程式的基本實現,可以脫離 spring 存在
(3)proxy 脫離 spring 可以直接使用的代理
(4)spring 結合 spring 的 AOP 模式
(5)mvc 結合 spring-mvc,也是最常用的一種模式
後續將考慮繼續優化,添加對於 springboot 的支持。
也考慮學習一下 shiro 的精髓,引入更多的特性。
開源地址
https://github.com/houbb/privilege
歡迎 fork/star~~
願景
框架可以支持更多的特性,比如基於角色,基於用戶。
開箱即用的設計,比如最基本的 sql + mybatis 的通用權限管理框架,類似於 quartz。
一個完整的項目設計,支持前後端頁面。
單獨的服務打磨,可以對外提供鑒權服務,結合 auth 等,進一步優化權限管理。
原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-hk/n/322865.html