复杂场景下的权限系统该怎么玩?ABAC权限模型帮你搞定它!
ABAC授權模型
常用的授權模型
ABAC的訪問控制
表達式語言
SpEL性能
ABAC實踐
crud代碼
security上下文
數據庫設計
java程序
測試類
Spring Security 和 Apache Shiro整合
最近有點無聊,以至于我很已經很久沒有寫出高質量的博客,為了挽回自己的這種無聊感打算手撕一下ABAC模型,畢竟RBAC模型五張基礎表玩來玩去也玩不出來多少火花了,整理一下ABAC模型的實現也讓這個周末更有意義一點。
第一章ABAC授權模型是為了讓大家先理解何為ABAC。
第二章ABAC實踐是對ABAC模型的代碼實現。
第三章簡單的描述了一下如何整合進Spring Security 或者 Apache Shiro。
ABAC授權模型
基于屬性的訪問控制(ABAC)- 阿里云IDaaS:
https://help.aliyun.com/document_detail/174235.html
ABAC- 百度百科:
https://baike.baidu.com/item/abac/3555041?fr=aladdin
個人覺得這兩篇文章已經完美描述了ABAC模型的原理
常用的授權模型
此節摘自基于屬性的訪問控制(ABAC)- 阿里云IDaaS
ACL(Access Control List)
在ACL中,包含用戶、資源、資源操作 三個關鍵要素。通過將資源以及資源操作授權給用戶而使用戶獲取對資源進行操作的權限。
RBAC(Role-Based Access Control )
是把用戶按角色進行歸類,通過用戶的角色來確定用戶能否針對某項資源進行某項操作。RBAC相對于ACL最大的優勢就是它簡化了用戶與權限的管理,通過對用戶進行分類,使得角色與權限關聯起來,而用戶與權限變成了間接關聯。
ABAC(Attribute Base Access Control)
基于屬性的權限控制不同于常見的將用戶通過某種方式關聯到權限的方式,ABAC則是通過動態計算一個或一組屬性來是否滿足某種條件來進行授權判斷(可以編寫簡單的邏輯)。
屬性通常來說分為四類:用戶屬性(如用戶年齡),環境屬性(如當前時間),操作屬性(如讀取)和對象屬性,所以理論上能夠實現非常靈活的權限控制,幾乎能滿足所有類型的需求。
ABAC的訪問控制
基于ABAC訪問控制需要動態計算實體的屬性、操作類型、相關的環境來控制是否有對操作對象的權限,所以在設計的時候需要考慮的條件判斷的靈活性、通用性、易用性,用戶只需要通過web頁面即可配置授權,這需要減少硬編碼似得邏輯變得簡單通用,那么這需要滿足一些運算符來實現。
| 算術運算符 | +, -, *, /, %, ^, div, mod |
| 關系運算符 | <, >, ==, !=, <=, >=, lt, gt, eq, ne, le, ge |
| 邏輯運算符 | and, or, not, &&, ? ||, ! |
| 條件 | ?: |
使用場景
用戶只需要配置 user.age > 20 的條件即可獲得特定的權限。
表達式語言
正如上一節所說的需要對某種條件進行解析那么就需要表達式語言,這讓我想起了Spring Framework的@Value注解和MyBatis的<if test=“”>
//?相信很多?Java?boy都使用過的吧 @Value("A?B:C") private?String?A;<select?id?=?"XXX"><if?test="user?!=?null">XXXX</if> </select>看到這里大家應該大致猜到了ABAC的的核心就是Expression Language(EL),雖然上面的代碼演示是使用Java生態作為演示,但是可以大膽的相信其他的編程語言都是有著自己的EL框架的。
java EL框架列表
spring-expression
OGNL
MVEL
JBoss EL
這里就不一一列舉了感興趣可以查看 Java EL生態排名:
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 包含一個基本的表達式編譯器。表達式通常被解釋,這在評估期間提供了很多動態靈活性,但沒有提供最佳性能。對于偶爾的表達式使用,這很好,但是,當由其他組件(如 Spring Integration)使用時,性能可能非常重要,并且沒有真正需要動態性。
SpEL 編譯器旨在滿足這一需求。在求值期間,編譯器生成一個 Java 類,它體現了運行時的表達式行為,并使用該類來實現更快的表達式求值。由于缺少表達式周圍的類型,編譯器在執行編譯時使用在表達式的解釋評估期間收集的信息。例如,它不能純粹從表達式中知道屬性引用的類型,但在第一次解釋評估期間,它會找出它是什么。當然,如果各種表達式元素的類型隨時間發生變化,基于此類派生信息進行編譯可能會在以后造成麻煩。出于這個原因,編譯最適合其類型信息在重復計算時不會改變的表達式。
考慮以下基本表達式:
someArray[0].someProperty.someOtherProperty?<?0.1由于前面的表達式涉及數組訪問、某些屬性取消引用和數字操作,因此性能提升非常顯著。在一個運行 50000 次迭代的微型基準測試示例中,使用解釋器評估需要 75 毫秒,使用表達式的編譯版本僅需要 3 毫秒。
有關SpEL的性能Spring官方描述說SpEL的性能很棒(個人感覺Spring對自己的測試結果是不是少打了一個0啊,3ms的時間有點無法理解)
ABAC實踐
本章僅實現ABAC的原理,不會對Spring Security和 Apache Shiro做任何的集成
因為筆者本人是一位Spring boy,所以工程項目會以Spring Boot框架作為基礎,使用其它編程語言的同學可能需要受苦一下了😂, 大家看懂原理就可以了。
Java 8
Spring Boot 2.7.6
MyBatis Plus 3.5.2
MySQL 8.0
數據庫設計
| user | 用戶表(ACL和RBAC都有這張表) |
| user_contribution | 用戶的附屬信息 (用戶屬性之類的,不能不一定只有這張表) |
| permission | 權限表達式(ACL和RBAC都有這張表) |
| abac | rbac表達式 |
| abac_permission | rbac表達式和權限的綁定關系, o2m |
java程序
因為篇幅問題, 只會使用必要的代碼, 代碼文件結構
|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 // Spring Boot啟動類| | | | | | |security| | | | | | | |MetadataCustomizer.java // 自定義user信息| | | | | | | |SecurityContext.java // Security上下文| | | | | | |entity| | | | | | | |Abac.java| | | | | | | |User.java| | | | | | |dao| | | | | | | |UserDao.java| | | | | | | |AbacDao.java| | | | | | |service| | | | | | | |UserService.java| | | | | | | |AbacService.java|pom.xmlcrud代碼
pom.xml
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.2</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>compile</scope></dependency> </dependencies>entity類
/***?@author?changjin?wei(魏昌進)*?@since?2022/11/26*/ @Data public?class?Abac?{private?Long?id;private?String?expression;/**?expression對應的permissions列表?*/@TableField(exist?=?false)private?List<String>?permissions; }@Data public?class?User?{private?Long?id;private?String?name;private?Integer?age;private?String?email;/**?用戶提交過倉庫?*/@TableField(exist?=?false)private?List<String>?contributions?=?new?ArrayList<>();/**?存放一些亂七八糟的數據,當然contributions字段也放在這里?*/@TableField(exist?=?false)private?Map<String,?Object>?metadata?=?new?HashMap<>(); }dao類
/***?@author?changjin?wei(魏昌進)*?@since?2022/11/26*/ @Mapper public?interface?AbacDao?extends?BaseMapper<Abac>?{/**?獲取abacId關聯權限?*/@Select("SELECT?p.permission\n"?+"FROM?abac_permission?ap?LEFT?JOIN?permission?p?ON?p.id?=?ap.permission_id\n"?+"WHERE?ap.abac_id?=?#{abacId}")List<String>?selectPermissions(Long?abacId);}/***?@author?changjin?wei(魏昌進)*?@since?2022/11/26*/ @Mapper public?interface?UserDao?extends?BaseMapper<User>?{/**?獲取用戶的倉庫信息?*/@Select("SELECT?repository?FROM?user_contribution?WHERE?user_id?=?#{userId}")List<String>?selectRepository(@Param("userId")?Long?userId); }service類
/***?@author?changjin?wei(魏昌進)*?@since?2022/11/26*/ @Service @RequiredArgsConstructor public?class?AbacService?{private?final?AbacDao?abacDao;/**?獲取abac表達式詳細信息列表?*/public?List<Abac>?getAll()?{List<Abac>?abacs?=?abacDao.selectList(null);for?(Abac?abac?:?abacs)?{List<String>?permissions?=?abacDao.selectPermissions(abac.getId());abac.setPermissions(permissions);}return?abacs;} }/***?@author?changjin?wei(魏昌進)*?@since?2022/11/26*/ @Service @RequiredArgsConstructor public?class?UserService?{private?final?UserDao?userDao;/**?根據userId獲取用戶詳細信息?*/public?User?get(Long?userId)?{User?user?=?userDao.selectById(userId);List<String>?repository?=?userDao.selectRepository(userId);user.setContributions(repository);return?user;} }security上下文
/***?自定義用戶元數據??用于獲取一些實體的屬性、操作類型、相關的環境**?@author?changjin?wei(魏昌進)*?@since?2022/11/26*/ public?interface?MetadataCustomizer?{/**?自定義用戶元數據?*/void?customize(User?user); }/***?解析abac表達式**?@author?changjin?wei(魏昌進)*?@since?2022/11/26*/ @Component public?class?SecurityContext?{/**?SpEL表達式解析器?*/private?final?ExpressionParser?expressionParser?=?new?SpelExpressionParser();/***??解析abac表達式*?@param?user?用戶詳細信息*?@param?abacs?abac表達式詳細信息集合*?@return?expressions集合,?將這個結果集存放到?Spring?Security?或者Apache?APISIX的userDetail上下文中*/public?List<String>?rbacPermissions(User?user,?List<Abac>?abacs)?{return?this.rbacPermissions(user,?abacs,?Collections.emptyList());}/***?解析abac表達式*?@param?user?用戶詳細信息*?@param?abacs?abac表達式詳細信息集合*?@param?metadataCustomizers??自定義用戶元數據??用于獲取一些實體的屬性、操作類型、相關的環境*?@return?expressions集合,?將這個結果集存放到?Spring?Security?或者Apache?APISIX的userDetail上下文中*/public?List<String>?rbacPermissions(User?user,?List<Abac>?abacs,?List<MetadataCustomizer>?metadataCustomizers)?{//?處理自定義元數據metadataCustomizers.forEach(metadataCustomizer?->?metadataCustomizer.customize(user));List<String>?expressions?=?new?ArrayList<>();for?(Abac?abac?:?abacs)?{//?解析表達式的求值器Expression?expression?=?expressionParser.parseExpression(abac.getExpression());//?創建環境上下文EvaluationContext?context?=?new?StandardEvaluationContext(user);//?獲取expression的結果if?(expression.getValue(context,?boolean.class))?{expressions.addAll(abac.getPermissions());}}return?expressions;}}測試類
/***?@author?changjin?wei(魏昌進)*?@since?2022/11/26*/ @SpringBootTest class?AbacApplicationTests?{@Autowiredprivate?UserService?userService;@Autowiredprivate?AbacService?abacService;@Autowiredprivate?SecurityContext?securityContext;/**?獲取不同用戶的abac權限?*/@Testvoid?testRbac()?{User?user?=?userService.get(1L);List<Abac>?rbac?=?abacService.getAll();List<String>?permissions?=?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);}/***?獲取自定義權限*/@Testvoid?testMetadataCustomizer()?{User?user?=?userService.get(1L);List<Abac>?rbac?=?abacService.getAll();List<String>?permissions?=?securityContext.rbacPermissions(user,?rbac);System.out.println(permissions);permissions?=?securityContext.rbacPermissions(user,?rbac,?getMetadataCustomizer());System.out.println(permissions);}/**?模擬網絡ip?*/private?List<MetadataCustomizer>?getMetadataCustomizer()?{return?new?ArrayList<MetadataCustomizer>()?{{add(user?->?user.getMetadata().put("ip",?"192.168.0.1"));}};} }DELETE?FROM?user; INSERT?INTO?user?(id,?name,?age,?email) VALUES?(1,?'魏昌進',?26,?'mail@wcj.plus'),(2,?'test',?1,?'mail1@wcj.plus'),(3,?'admin',?1,?'mail2@wcj.plus');DELETE?FROM?user_contribution; INSERT?INTO?user_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');DELETE?FROM?permission; INSERT?INTO?permission?(id,?permission) VALUES?(1,?'github:pr:merge'),(2,?'github:pr:close'),(3,?'github:pr:open'),(4,?'github:pr:comment');DELETE?FROM?abac; INSERT?INTO?abac?(id,?expression) VALUES?(1,?'contributions.contains(''galaxy-sea/spring-cloud-apisix'')'),(2,?'name?==?''admin'''),(3,?'metadata.get(''ip'')?==?''192.168.0.1''');DELETE?FROM?abac_permission; INSERT?INTO?abac_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轉換為GrantedAuthority即可
/***?這里是偽代碼,?展示一下大概邏輯**?@author?changjin?wei(魏昌進)*?@since?2022/04/29*/ public?class?IamOncePerRequestFilter?implements?OncePerRequestFilter?{@Autowiredprivate?SecurityContext?securityContext;@Autowiredprivate?AbacService?abacService;@Autowiredprivate?List<MetadataCustomizer>?metadataCustomizers;@Autowiredpublic?void?doFilterInternal(HttpServletRequest?request,?HttpServletResponse?response,?FilterChain?filterChain)?{UserDetails?user?=?toUser();List<String>?permissions?=?securityContext.rbacPermissions(user,?abacService.getAll(),?metadataCustomizers);List<GrantedAuthority>?abacAuthority?=?permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());user.getAuthorities().addAll(abacAuthority);} }項目源碼:
https://github.com/galaxy-sea/galaxy-blogs/tree/master/code/abac
推薦
Java面試題寶典
技術內卷群,一起來學習!!
PS:因為公眾號平臺更改了推送規則,如果不想錯過內容,記得讀完點一下“在看”,加個“星標”,這樣每次新文章推送才會第一時間出現在你的訂閱列表里。點“在看”支持我們吧!
總結
以上是生活随笔為你收集整理的复杂场景下的权限系统该怎么玩?ABAC权限模型帮你搞定它!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2020山东计算机大赛裴鹏飞,裴鹏飞个人
- 下一篇: java信息管理系统总结_java实现科