ABAC授權(quán)模型
基于屬性的訪問控制(ABAC)- 阿里云IDaaS:
- https://help.aliyun.com/document_detail/174235.html
ABAC- 百度百科:
個人覺得這兩篇文章已經(jīng)完美描述了ABAC模型的原理
常用的授權(quán)模型
此節(jié)摘自基于屬性的訪問控制(ABAC)- 阿里云IDaaS
- ACL(Access Control List)
在ACL中,包含用戶、資源、資源操作 三個關(guān)鍵要素。通過將資源以及資源操作授權(quán)給用戶而使用戶獲取對資源進(jìn)行操作的權(quán)限。
- RBAC(Role-Based Access Control )
是把用戶按角色進(jìn)行歸類,通過用戶的角色來確定用戶能否針對某項資源進(jìn)行某項操作。RBAC相對于ACL最大的優(yōu)勢就是它簡化了用戶與權(quán)限的管理,通過對用戶進(jìn)行分類,使得角色與權(quán)限關(guān)聯(lián)起來,而用戶與權(quán)限變成了間接關(guān)聯(lián)。
- ABAC(Attribute Base Access Control)
基于屬性的權(quán)限控制不同于常見的將用戶通過某種方式關(guān)聯(lián)到權(quán)限的方式,ABAC則是通過動態(tài)計算一個或一組屬性來是否滿足某種條件來進(jìn)行授權(quán)判斷(可以編寫簡單的邏輯)。
屬性通常來說分為四類:用戶屬性(如用戶年齡),環(huán)境屬性(如當(dāng)前時間),操作屬性(如讀?。┖蛯ο髮傩?,所以理論上能夠?qū)崿F(xiàn)非常靈活的權(quán)限控制,幾乎能滿足所有類型的需求。
ABAC的訪問控制
基于ABAC訪問控制需要動態(tài)計算實體的屬性、操作類型、相關(guān)的環(huán)境來控制是否有對操作對象的權(quán)限,所以在設(shè)計的時候需要考慮的條件判斷的靈活性、通用性、易用性,用戶只需要通過web頁面即可配置授權(quán),這需要減少硬編碼似得邏輯變得簡單通用,那么這需要滿足一些運算符來實現(xiàn)。
類型 | 運算符 |
---|---|
算術(shù)運算符 | +, -, *, /, %, ^, div, mod |
關(guān)系運算符 | <, >, ==, !=, <=, >=, lt, gt, eq, ne, le, ge |
邏輯運算符 | and, or, not, &&, ||, ! |
條件 | ?: |
使用場景
用戶只需要配置 user.age > 20
的條件即可獲得特定的權(quán)限。
表達(dá)式語言
正如上一節(jié)所說的需要對某種條件進(jìn)行解析那么就需要表達(dá)式語言,這讓我想起了Spring Framework
的@Value
注解和MyBatis
的
//相信很多Javaboy都使用過的吧
@Value("A?B:C")
privateStringA;
看到這里大家應(yīng)該大致猜到了ABAC的的核心就是Expression Language(EL)
,雖然上面的代碼演示是使用Java生態(tài)作為演示,但是可以大膽的相信其他的編程語言都是有著自己的EL框架的。
java EL框架列表
- spring-expression
- OGNL
- MVEL
- JBoss EL
這里就不一一列舉了感興趣可以查看 Java EL生態(tài)排名:
https://mvnrepository.com/open-source/expression-languages
SpEL性能
Spring Expression Language (SpEL)官方文檔:
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions
Spring官方文檔摘取 翻譯
Spring Framework 4.1 包含一個基本的表達(dá)式編譯器。表達(dá)式通常被解釋,這在評估期間提供了很多動態(tài)靈活性,但沒有提供最佳性能。對于偶爾的表達(dá)式使用,這很好,但是,當(dāng)由其他組件(如 Spring Integration)使用時,性能可能非常重要,并且沒有真正需要動態(tài)性。
SpEL 編譯器旨在滿足這一需求。在求值期間,編譯器生成一個 Java 類,它體現(xiàn)了運行時的表達(dá)式行為,并使用該類來實現(xiàn)更快的表達(dá)式求值。由于缺少表達(dá)式周圍的類型,編譯器在執(zhí)行編譯時使用在表達(dá)式的解釋評估期間收集的信息。例如,它不能純粹從表達(dá)式中知道屬性引用的類型,但在第一次解釋評估期間,它會找出它是什么。當(dāng)然,如果各種表達(dá)式元素的類型隨時間發(fā)生變化,基于此類派生信息進(jìn)行編譯可能會在以后造成麻煩。出于這個原因,編譯最適合其類型信息在重復(fù)計算時不會改變的表達(dá)式。
考慮以下基本表達(dá)式:
someArray[0].someProperty.someOtherProperty0.1
由于前面的表達(dá)式涉及數(shù)組訪問、某些屬性取消引用和數(shù)字操作,因此性能提升非常顯著。在一個運行 50000 次迭代的微型基準(zhǔn)測試示例中,使用解釋器評估需要 75 毫秒,使用表達(dá)式的編譯版本僅需要 3 毫秒。
有關(guān)SpEL的性能Spring官方描述說SpEL的性能很棒(個人感覺Spring對自己的測試結(jié)果是不是少打了一個0啊,3ms的時間有點無法理解)
基于 Spring Boot + MyBatis Plus + Vue & Element 實現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
- 項目地址:https://github.com/YunaiV/ruoyi-vue-pro
- 視頻教程:https://doc.iocoder.cn/video/
ABAC實踐
本章僅實現(xiàn)ABAC的原理,不會對Spring Security和 Apache Shiro做任何的集成
因為筆者本人是一位Spring boy,所以工程項目會以Spring Boot框架作為基礎(chǔ),使用其它編程語言的同學(xué)可能需要受苦一下了, 大家看懂原理就可以了。
- Java 8
- Spring Boot 2.7.6
- MyBatis Plus 3.5.2
- MySQL 8.0
數(shù)據(jù)庫設(shè)計
圖片表名 | 作用 |
---|---|
user | 用戶表(ACL和RBAC都有這張表) |
user_contribution | 用戶的附屬信息 (用戶屬性之類的,不能不一定只有這張表) |
permission | 權(quán)限表達(dá)式(ACL和RBAC都有這張表) |
abac | rbac表達(dá)式 |
abac_permission | rbac表達(dá)式和權(quán)限的綁定關(guān)系, o2m |
>基于SpringCloudAlibaba+Gateway+Nacos+RocketMQ+Vue&Element實現(xiàn)的后臺管理系統(tǒng)+用戶小程序,支持RBAC動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
>
>*項目地址://github.com/YunaiV/yudao-cloud>
>*視頻教程://doc.iocoder.cn/video/>
#用戶表
DROPTABLEifEXISTSuser;
CREATETABLEuser(
idbigint(20)NOTNULLCOMMENT'主鍵ID',
namevarchar(30)NULLDEFAULTNULLCOMMENT'姓名',
ageint(11)NULLDEFAULTNULLCOMMENT'年齡',
emailvarchar(50)NULLDEFAULTNULLCOMMENT'郵箱',
PRIMARYKEY(id)
);
#用戶邊緣數(shù)據(jù)
DROPTABLEifEXISTSuser_contribution;
CREATETABLEuser_contribution(
idbigint(20)NOTNULLCOMMENT'主鍵ID',
user_idbigint(20)NOTNULLCOMMENT'用戶表ID',
repositoryvarchar(100)NOTNULLCOMMENT'倉庫',
PRIMARYKEY(id)
);
#權(quán)限表
DROPTABLEifEXISTSpermission;
CREATETABLEpermission(
idbigint(20)NOTNULLCOMMENT'主鍵ID',
permissionvarchar(100)NOTNULLCOMMENT'權(quán)限名稱',
PRIMARYKEY(id)
);
#abac表達(dá)式表
DROPTABLEifEXISTSabac;
CREATETABLEabac(
idbigint(20)NOTNULLCOMMENT'主鍵ID',
expressionvarchar(100)NOTNULLCOMMENT'abac表達(dá)式',
PRIMARYKEY(id)
);
#abac表和權(quán)限表的關(guān)聯(lián)表,o2m
DROPTABLEifEXISTSabac_permission;
CREATETABLEabac_permission(
idbigint(20)NOTNULLCOMMENT'主鍵ID',
abac_idbigint(20)NOTNULLCOMMENT'abac表ID',
permission_idbigint(20)NOTNULLCOMMENT'permission表ID',
PRIMARYKEY(id)
);
java程序
因為篇幅問題, 只會使用必要的代碼, 代碼文件結(jié)構(gòu)
|src
||test
|||java
||||plus.wcj.abac.AbacApplicationTests.java測試類代碼
||main
|||resources
||||application.yml
||||db
|||||schema-h2.sql//DDL
|||||data-h2.sql//DML
|||java
||||plus.wcj.abac
|||||||AbacApplication.java//SpringBoot啟動類
|||||||security
||||||||MetadataCustomizer.java//自定義user信息
||||||||SecurityContext.java//Security上下文
|||||||entity
||||||||Abac.java
||||||||User.java
|||||||dao
||||||||UserDao.java
||||||||AbacDao.java
|||||||service
||||||||UserService.java
||||||||AbacService.java
|pom.xml
crud代碼
pom.xml
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-test
test
com.baomidou
mybatis-plus-boot-starter
3.5.2
mysql
mysql-connector-java
org.projectlombok
lombok
compile
entity類
/**
*@authorchangjinwei(魏昌進(jìn))
*@since2022/11/26
*/
@Data
publicclassAbac{
privateLongid;
privateStringexpression;
/**expression對應(yīng)的permissions列表*/
@TableField(exist=false)
privateListpermissions;
}
@Data
publicclassUser{
privateLongid;
privateStringname;
privateIntegerage;
privateStringemail;
/**用戶提交過倉庫*/
@TableField(exist=false)
privateListcontributions=newArrayList<>();
/**存放一些亂七八糟的數(shù)據(jù),當(dāng)然contributions字段也放在這里*/
@TableField(exist=false)
privateMapmetadata=newHashMap<>();
}
dao類
/**
*@authorchangjinwei(魏昌進(jìn))
*@since2022/11/26
*/
@Mapper
publicinterfaceAbacDaoextendsBaseMapper<Abac>{
/**獲取abacId關(guān)聯(lián)權(quán)限*/
@Select("SELECTp.permission
"+
"FROMabac_permissionapLEFTJOINpermissionpONp.id=ap.permission_id
"+
"WHEREap.abac_id=#{abacId}")
ListselectPermissions(LongabacId) ;
}
/**
*@authorchangjinwei(魏昌進(jìn))
*@since2022/11/26
*/
@Mapper
publicinterfaceUserDaoextendsBaseMapper<User>{
/**獲取用戶的倉庫信息*/
@Select("SELECTrepositoryFROMuser_contributionWHEREuser_id=#{userId}")
ListselectRepository(@Param("userId")LonguserId) ;
}
service類
/**
*@authorchangjinwei(魏昌進(jìn))
*@since2022/11/26
*/
@Service
@RequiredArgsConstructor
publicclassAbacService{
privatefinalAbacDaoabacDao;
/**獲取abac表達(dá)式詳細(xì)信息列表*/
publicListgetAll() {
Listabacs=abacDao.selectList(null);
for(Abacabac:abacs){
Listpermissions=abacDao.selectPermissions(abac.getId());
abac.setPermissions(permissions);
}
returnabacs;
}
}
/**
*@authorchangjinwei(魏昌進(jìn))
*@since2022/11/26
*/
@Service
@RequiredArgsConstructor
publicclassUserService{
privatefinalUserDaouserDao;
/**根據(jù)userId獲取用戶詳細(xì)信息*/
publicUserget(LonguserId){
Useruser=userDao.selectById(userId);
Listrepository=userDao.selectRepository(userId);
user.setContributions(repository);
returnuser;
}
}
security上下文
/**
*自定義用戶元數(shù)據(jù)用于獲取一些實體的屬性、操作類型、相關(guān)的環(huán)境
*
*@authorchangjinwei(魏昌進(jìn))
*@since2022/11/26
*/
publicinterfaceMetadataCustomizer{
/**自定義用戶元數(shù)據(jù)*/
voidcustomize(Useruser);
}
/**
*解析abac表達(dá)式
*
*@authorchangjinwei(魏昌進(jìn))
*@since2022/11/26
*/
@Component
publicclassSecurityContext{
/**SpEL表達(dá)式解析器*/
privatefinalExpressionParserexpressionParser=newSpelExpressionParser();
/**
*解析abac表達(dá)式
*@paramuser用戶詳細(xì)信息
*@paramabacsabac表達(dá)式詳細(xì)信息集合
*@returnexpressions集合,將這個結(jié)果集存放到SpringSecurity或者ApacheAPISIX的userDetail上下文中
*/
publicListrbacPermissions(Useruser,Listabacs) {
returnthis.rbacPermissions(user,abacs,Collections.emptyList());
}
/**
*解析abac表達(dá)式
*@paramuser用戶詳細(xì)信息
*@paramabacsabac表達(dá)式詳細(xì)信息集合
*@parammetadataCustomizers自定義用戶元數(shù)據(jù)用于獲取一些實體的屬性、操作類型、相關(guān)的環(huán)境
*@returnexpressions集合,將這個結(jié)果集存放到SpringSecurity或者ApacheAPISIX的userDetail上下文中
*/
publicListrbacPermissions(Useruser,Listabacs,ListmetadataCustomizers) {
//處理自定義元數(shù)據(jù)
metadataCustomizers.forEach(metadataCustomizer->metadataCustomizer.customize(user));
Listexpressions=newArrayList<>();
for(Abacabac:abacs){
//解析表達(dá)式的求值器
Expressionexpression=expressionParser.parseExpression(abac.getExpression());
//創(chuàng)建環(huán)境上下文
EvaluationContextcontext=newStandardEvaluationContext(user);
//獲取expression的結(jié)果
if(expression.getValue(context,boolean.class)){
expressions.addAll(abac.getPermissions());
}
}
returnexpressions;
}
}
測試類
/**
*@authorchangjinwei(魏昌進(jìn))
*@since2022/11/26
*/
@SpringBootTest
classAbacApplicationTests{
@Autowired
privateUserServiceuserService;
@Autowired
privateAbacServiceabacService;
@Autowired
privateSecurityContextsecurityContext;
/**獲取不同用戶的abac權(quán)限*/
@Test
voidtestRbac(){
Useruser=userService.get(1L);
Listrbac=abacService.getAll();
Listpermissions=securityContext.rbacPermissions(user,rbac);
System.out.println(permissions);
user=userService.get(2L);
permissions=securityContext.rbacPermissions(user,rbac);
System.out.println(permissions);
user=userService.get(3L);
permissions=securityContext.rbacPermissions(user,rbac);
System.out.println(permissions);
}
/**
*獲取自定義權(quán)限
*/
@Test
voidtestMetadataCustomizer(){
Useruser=userService.get(1L);
Listrbac=abacService.getAll();
Listpermissions=securityContext.rbacPermissions(user,rbac);
System.out.println(permissions);
permissions=securityContext.rbacPermissions(user,rbac,getMetadataCustomizer());
System.out.println(permissions);
}
/**模擬網(wǎng)絡(luò)ip*/
privateListgetMetadataCustomizer() {
returnnewArrayList(){{
add(user->user.getMetadata().put("ip","192.168.0.1"));
}};
}
}
DELETEFROMuser;
INSERTINTOuser(id,name,age,email)
VALUES(1,'魏昌進(jìn)',26,'mail@wcj.plus'),
(2,'test',1,'mail1@wcj.plus'),
(3,'admin',1,'mail2@wcj.plus');
DELETEFROMuser_contribution;
INSERTINTOuser_contribution(id,user_id,repository)
VALUES(1,1,'galaxy-sea/spring-cloud-apisix'),
(2,2,'spring-cloud/spring-cloud-commons'),
(3,2,'spring-cloud/spring-cloud-openfeign'),
(4,2,'alibaba/spring-cloud-alibaba'),
(5,2,'Tencent/spring-cloud-tencent'),
(6,2,'apache/apisix-docker');
DELETEFROMpermission;
INSERTINTOpermission(id,permission)
VALUES(1,'githubmerge'),
(2,'githubclose'),
(3,'githubopen'),
(4,'githubcomment');
DELETEFROMabac;
INSERTINTOabac(id,expression)
VALUES(1,'contributions.contains(''galaxy-sea/spring-cloud-apisix'')'),
(2,'name==''admin'''),
(3,'metadata.get(''ip'')==''192.168.0.1''');
DELETEFROMabac_permission;
INSERTINTOabac_permission(id,abac_id,permission_id)
VALUES(1,1,1),
(2,2,1),
(3,2,2),
(4,2,3),
(5,2,4),
(6,3,1),
(7,3,2),
(8,3,3),
(9,3,4);
Spring Security 和 Apache Shiro整合
Spring Security只需要修改攔截器即可在獲取到UserDetails
將SecurityContext#rbacPermissions
轉(zhuǎn)換為GrantedAuthority
即可
/**
*這里是偽代碼,展示一下大概邏輯
*
*@authorchangjinwei(魏昌進(jìn))
*@since2022/04/29
*/
publicclassIamOncePerRequestFilterimplementsOncePerRequestFilter{
@Autowired
privateSecurityContextsecurityContext;
@Autowired
privateAbacServiceabacService;
@Autowired
privateListmetadataCustomizers;
@Autowired
publicvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,FilterChainfilterChain){
UserDetailsuser=toUser();
Listpermissions=securityContext.rbacPermissions(user,abacService.getAll(),metadataCustomizers);
ListabacAuthority=permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
user.getAuthorities().addAll(abacAuthority);
}
}
項目源碼:
- https://github.com/galaxy-sea/galaxy-blogs/tree/master/code/abac
-
編碼
+關(guān)注
關(guān)注
6文章
933瀏覽量
54731 -
權(quán)限系統(tǒng)
+關(guān)注
關(guān)注
0文章
6瀏覽量
5944 -
阿里云
+關(guān)注
關(guān)注
3文章
934瀏覽量
42932
原文標(biāo)題:復(fù)雜場景下的權(quán)限系統(tǒng)該怎么玩?ABAC權(quán)限模型幫你搞定它!
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論