Spring Security基于數(shù)據(jù)庫的ABAC屬性權(quán)限模型實戰(zhàn)開發(fā)教程
1. 前言
今天博主又抽空來給小伙伴更新 Spring Security
教程啦,上個章節(jié)中我們講解了如何通過數(shù)據(jù)庫實現(xiàn)基于數(shù)據(jù)庫的動態(tài)用戶認(rèn)證,大家可能發(fā)現(xiàn)了,項目中是基于RBAC角色模型的權(quán)限控制,雖然能滿足大多數(shù)場景,但在面對復(fù)雜、細(xì)粒度的權(quán)限需求時可能會力不從心?;趯傩缘脑L問控制(ABAC)模型則通過評估用戶、資源、環(huán)境等多種屬性,實現(xiàn)更加靈活的權(quán)限控制。
例如,某個菜單的訪問可能不僅取決于用戶角色,還取決于用戶的部門、時間或其他屬性。因此,需要在權(quán)限驗證時動態(tài)獲取這些屬性,并進(jìn)行評估。那么本章節(jié)我們就來講解基于數(shù)據(jù)庫的ABAC屬性權(quán)限模型實戰(zhàn)開發(fā)
2. 權(quán)限決策依據(jù)
既然談到了 RBAC
和 ABAC
兩個模型,就大家介紹下兩者間的區(qū)別:
RBAC
- 核心思想:以角色作為權(quán)限管理的核心,每個用戶被賦予一個或多個角色,而角色與權(quán)限之間存在固定的映射關(guān)系。
- 決策依據(jù):當(dāng)用戶請求訪問資源時,系統(tǒng)根據(jù)用戶所屬角色所擁有的權(quán)限進(jìn)行校驗。
- 粒度:粒度相對較粗,因為權(quán)限是綁定在角色上的,無法針對單個請求條件進(jìn)行動態(tài)決策。
ABAC
- 核心思想:以屬性(Attribute)為基礎(chǔ),利用用戶屬性、資源屬性、環(huán)境屬性等多個維度的條件進(jìn)行權(quán)限判斷。
- 決策依據(jù):權(quán)限決策是基于各種屬性之間的邏輯表達(dá)式和策略規(guī)則來動態(tài)確定是否允許訪問。
- 粒度:支持非常細(xì)粒度的控制,可以針對具體屬性制定規(guī)則,實現(xiàn)精準(zhǔn)的權(quán)限控制。
綜合對比
對比維度 | RBAC | ABAC |
---|---|---|
決策依據(jù) | 用戶所屬角色與預(yù)定義權(quán)限映射關(guān)系 | 用戶、資源及環(huán)境屬性和策略規(guī)則 |
靈活性 | 固定、靜態(tài)權(quán)限模型 | 動態(tài)、可擴(kuò)展的權(quán)限決策模型 |
管理難度 | 管理較簡單,但角色關(guān)系復(fù)雜時易混亂 | 規(guī)則管理復(fù)雜,但擴(kuò)展靈活 |
粒度 | 較粗,難以細(xì)化至個性化條件 | 非常細(xì)粒度,可實現(xiàn)精確權(quán)限控制 |
適用場景 | 企業(yè)內(nèi)部、權(quán)限固定的系統(tǒng) | 復(fù)雜、多變、動態(tài)決策的業(yè)務(wù)系統(tǒng) |
3. 數(shù)據(jù)庫表結(jié)構(gòu)說明
上一個章節(jié)RBAC角色模型我們使用了五張表,sys_user
、 sys_role
、 sys_user_role
、sys_menu
、 sys_role_menu
,需要數(shù)據(jù)表結(jié)構(gòu)的小伙伴可以查閱上一章內(nèi)容!本章節(jié)不再贅述
現(xiàn)在我們在傳統(tǒng)RBAC模型基礎(chǔ)上,加入ABAC屬性權(quán)限模型 ABAC(Attribute-Based Access Control)
引入了更細(xì)粒度的動態(tài)控制維度:
為什么要增加ABAC屬性權(quán)限模型?
需求1:請求某個方法除了要驗證用戶角色或菜單資源,我還要判斷用戶屬性部門=IT,國家是ZH
需求2:請求某個方法除了要驗證用戶角色或菜單資源,我還要限制訪問時間段
而如果使用ABAC屬性權(quán)限模型動態(tài)策略就可以很輕松解決這樣的問題!
基于上述的需求,我們來擴(kuò)展我們的數(shù)據(jù)庫表,sys_user_attr
為用戶相關(guān)屬性,sys_policy
為策略表(ABAC規(guī)則存儲)
-- 擴(kuò)展用戶屬性表(新增) CREATE TABLE sys_user_attr ( user_id BIGINT NOT NULL, attr_key VARCHAR(50) NOT NULL, attr_value VARCHAR(100) NOT NULL, PRIMARY KEY (user_id, attr_key) ); -- 示例數(shù)據(jù) INSERT INTO sys_user_attr VALUES (1, 'department', 'IT'), (2, 'department', 'HR'), (3, 'security_level', '3'); (1, 'country', 'zh'),
權(quán)限策略表設(shè)計:
存儲 ABAC
策略,每條策略包含一個條件表達(dá)式(基于 SpEL
編寫)
CREATE TABLE sys_policy ( policy_id BIGINT AUTO_INCREMENT PRIMARY KEY, policy_name VARCHAR(50) NOT NULL, target_resource VARCHAR(64) NOT NULL, condition_expression VARCHAR(255) NOT NULL ); INSERT INTO sys_policy VALUES (1, 'IT部門訪問策略', 'admin:menu', "#user.attrs['department'] == 'IT'"), (2, '高安全級別策略', 'developers:menu', "T(Integer).parseInt(#user.attrs['security_level']) >= 3"); (3, 'IT部門訪問策略', 'admin:menu', "#user.attrs['country'] == 'zh'");
整體數(shù)據(jù)庫結(jié)構(gòu)如下:
4. 實戰(zhàn)開始
接下來在之前的 Maven
項目中,我們復(fù)用上個章節(jié)的子模塊并命名 abac-spring-security
由于涉及數(shù)據(jù)庫操作以及整合mybatis-plus,上一章節(jié)博主已經(jīng)進(jìn)行了配置的詳解這里就簡單貼出代碼供大家參考:
<!--Lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <!--使用 HikariCP 連接池--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- mysql驅(qū)動 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <!-- mybatis-plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.9</version> </dependency>
配置yml文件,運行項目確保項目能正常鏈接數(shù)據(jù)庫且啟動成功
server: port: 8084 spring: application: name: db-spring-security #最新Spring Security實戰(zhàn)教程(六)基于數(shù)據(jù)庫的ABAC屬性權(quán)限模型實戰(zhàn)開發(fā) datasource: url: jdbc:mysql://localhost:3306/slave_db?useSSL=false&serverTimezone=UTC username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 5 mybatis-plus: configuration: map-underscore-to-camel-case: true # 開啟駝峰轉(zhuǎn)換 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL cache-enabled: true # 開啟二級緩存 global-config: db-config: logic-delete-field: delFlag # 邏輯刪除字段 logic-delete-value: 1 # 刪除值 logic-not-delete-value: 0 # 未刪除值
5. MyBatis-Plus實體定義
接下來我們開始編寫業(yè)務(wù)代碼
? 用戶實體(實現(xiàn)UserDetails)
@Data @TableName("sys_user") public class SysUser implements UserDetails { @TableId(type = IdType.AUTO) private Long userId; @TableField("login_name") private String username; // Spring Security認(rèn)證使用的字段 private String password; private String status; // 狀態(tài)(0正常 1鎖定) private String delFlag; // 刪除標(biāo)志(0代表存在 1代表刪除) @TableField(exist = false) private List<SysRole> roles; @TableField(exist = false) private Map<String, String> attrs; // 用戶的屬性集合,用于 ABAC 動態(tài)權(quán)限評估 // 實現(xiàn)UserDetails接口 @Override public Collection<? extends GrantedAuthority> getAuthorities() { // 組裝 GrantedAuthority 集合,將角色和菜單權(quán)限都加入 Set<GrantedAuthority> authorities = new HashSet<>(); authorities.addAll(roles.stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getRoleKey())) .collect(Collectors.toList())); authorities.addAll(roles.stream() .flatMap(role -> role.getMenus().stream()) .map(menu -> new SimpleGrantedAuthority(menu.getPerms())) .collect(Collectors.toList())); return authorities; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return "0".equals(status); } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return "0".equals(delFlag); } }
? 角色實體
@Data @TableName("sys_role") public class SysRole { @TableId(type = IdType.AUTO) private Long roleId; private String roleName; private String roleKey; @TableField(exist = false) private List<SysMenu> menus; }
? 菜單實體
@Data @TableName("sys_menu") public class SysMenu { @TableId(type = IdType.AUTO) private Long menuId; private String menuName; private String perms; }
? 用戶屬性實體
@Data public class SysUserAttr { private Long userId; private String attrKey; private String attrValue; }
? 決策表實體
@Data @TableName("sys_policy") public class SysPolicy { @TableId(type = IdType.AUTO) private Long policyId; private String policyName; private String targetResource; private String conditionExpression; }
6. MyBatis-Plus Mapper配置
除了 UserMapper
增加 selectUserAttrByUserId
方法以及新增 SysPolicyMapper
,其余代碼與上個章節(jié)一致!
UserMapper接口 : 主要是通過用戶角色中間表獲取角色信息(角色信息中又包含了菜單信息)
@Mapper public interface UserMapper extends BaseMapper<SysUser> { @Select("SELECT r.* FROM sys_role r " + "JOIN sys_user_role ur ON r.role_id = ur.role_id " + "WHERE ur.user_id = #{userId}") @Results({ @Result(property = "roleId", column = "role_id"), @Result(property = "menus", column = "role_id", many = @Many(select = "com.toher.springsecurity.demo.abac.mapper.MenuMapper.selectByUserId")) }) List<SysRole> selectRolesByUserId(Long userId); /** * 獲取用戶屬性 * @param userId * @return */ @Select("SELECT * FROM sys_user_attr WHERE user_id = #{userId}") List<SysUserAttr> selectUserAttrByUserId(Long userId); }
RoleMapper接口 : 主要是通過角色菜單中間表獲取菜單信息
@Mapper public interface RoleMapper extends BaseMapper<SysRole> { @Select("SELECT m.* FROM sys_menu m " + "JOIN sys_role_menu rm ON m.menu_id = rm.menu_id " + "WHERE rm.role_id = #{roleId}") List<SysMenu> selectMenusByRoleId(Long roleId); }
RoleMapper接口 : 主要是通過角色菜單中間表獲取菜單信息
@Mapper public interface MenuMapper extends BaseMapper<SysMenu> { @Select("SELECT DISTINCT m.* FROM sys_menu m " + "JOIN sys_role_menu rm ON m.menu_id = rm.menu_id " + "JOIN sys_user_role ur ON rm.role_id = ur.role_id " + "WHERE ur.user_id = #{userId}") List<SysMenu> selectByUserId(Long userId); }
SysPolicyMapper接口
@Mapper public interface SysPolicyMapper extends BaseMapper<SysPolicy> { }
7. 自定義UserDetailsService實現(xiàn)
自定義 UserDetailsService
繼承 UserDetailsService
,重寫 loadUserByUsername
方法,注入 UserMapper
以及 roleMapper
通過用戶名查詢數(shù)據(jù)庫數(shù)據(jù),同時將用戶的角色、菜單資源集合、用戶屬性集合 一并賦值;
@Service @RequiredArgsConstructor public class UserDetailsServiceImpl implements UserDetailsService { private final UserMapper userMapper; private final RoleMapper roleMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 1. 查詢基礎(chǔ)用戶信息 LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(SysUser::getUsername, username); SysUser user = userMapper.selectOne(wrapper); if (user == null) { throw new UsernameNotFoundException("用戶不存在"); } // 2. 加載角色和權(quán)限 List<SysRole> roles = userMapper.selectRolesByUserId(user.getUserId()); roles.forEach(role -> role.setMenus(roleMapper.selectMenusByRoleId(role.getRoleId())) ); user.setRoles(roles); // 3. 檢查賬戶狀態(tài) if (!user.isEnabled()) { throw new DisabledException("用戶已被禁用"); } // 4. 用戶的屬性集合,用于 ABAC 動態(tài)權(quán)限評估 List<SysUserAttr> attrs = userMapper.selectUserAttrByUserId(user.getUserId()); // 轉(zhuǎn)成map集合 user.setAttrs(attrs.stream() .collect(Collectors.toMap(s -> s.getAttrKey(), s -> s.getAttrValue()))); return user; } }
8. 實現(xiàn)方式一:自定義MethodSecurityExpressionHandler
編寫策略決策引擎
@Component public class AbacDecisionEngine { private final SpelExpressionParser parser = new SpelExpressionParser(); @Autowired private SysPolicyMapper sysPolicyMapper; public boolean check(Authentication authentication, String resource) { SysUser userDetails = (SysUser) authentication.getPrincipal(); // 加載策略集 LambdaQueryWrapper<SysPolicy> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(SysPolicy::getTargetResource, resource); List<SysPolicy> policies = sysPolicyMapper.selectList(queryWrapper); if (policies.isEmpty()) { return false; } // 構(gòu)建評估上下文 EvaluationContext context = new StandardEvaluationContext(); // 將用戶傳入表達(dá)式上下文 如:#user.attrs['department'] == 'IT' // 其中user前綴就是我們傳入的user context.setVariable("user", userDetails); return policies.stream().allMatch(policy -> parser.parseExpression(policy.getConditionExpression()).getValue(context, Boolean.class) ); } }
重寫PermissionEvaluator
@RequiredArgsConstructor public class AbacPermissionEvaluator implements PermissionEvaluator { private final AbacDecisionEngine abacEngine; @Override public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) { return abacEngine.check(auth, (String)permission); } @Override public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) { // 本示例僅實現(xiàn) hasPermission(Authentication, Object, Object) return false; } }
9. 實現(xiàn)方式二:自定義注解
使用自定義注解,Spring Security
將在每次方法調(diào)用時調(diào)用該 bean
上給定的方法
@Component("authz") @RequiredArgsConstructor public class AuthorizationLogic { private final SpelExpressionParser parser = new SpelExpressionParser(); private final SysPolicyMapper sysPolicyMapper; public boolean check(MethodSecurityExpressionOperations operations, String permission) { SysUser userDetails = (SysUser) operations.getAuthentication().getPrincipal(); // 加載策略集 LambdaQueryWrapper<SysPolicy> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(SysPolicy::getTargetResource, permission); List<SysPolicy> policies = sysPolicyMapper.selectList(queryWrapper); if (policies.isEmpty()) { return false; } // 構(gòu)建評估上下文 EvaluationContext context = new StandardEvaluationContext(); // 將用戶傳入表達(dá)式上下文 如:#user.attrs['department'] == 'IT' // 其中user前綴就是我們傳入的user context.setVariable("user", userDetails); return policies.stream().allMatch(policy -> parser.parseExpression(policy.getConditionExpression()).getValue(context, Boolean.class) ); } }
10. Spring Security配置文件
@Configuration //開啟方法級的安全控制 @EnableMethodSecurity @RequiredArgsConstructor public class AbacSecurityConfig { private final UserDetailsServiceImpl userDetailsService; private final AbacDecisionEngine abacEngine; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http. authorizeHttpRequests(authorize -> authorize .requestMatchers("/setPassword").permitAll() //配置形式ADMIN角色可以訪問/admin/view .requestMatchers("/admin/view").hasRole("ADMIN") .anyRequest().authenticated()) .userDetailsService(userDetailsService) .formLogin(withDefaults()) .logout(withDefaults()) ; return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 配置 Method Security Expression Handler,使用自定義的 PermissionEvaluator */ @Bean public MethodSecurityExpressionHandler methodSecurityExpressionHandler(AbacDecisionEngine abacEngine) { DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); handler.setPermissionEvaluator(new AbacPermissionEvaluator(abacEngine)); return handler; }
11. controller測試文件
新增一個 AdminController
作為ABAC屬性權(quán)限模型
的測試
@RestController @RequestMapping("/api") public class AdminController { /** * MethodSecurityExpressionHandler方式 * * @return */ @PreAuthorize("hasPermission(null, 'admin:menu')") @GetMapping("/admin") public ResponseEntity<?> getAdminData() { return ResponseEntity.ok("MethodSecurityExpressionHandler方式"); } /** * 自定義注解的方式 * @return */ @PreAuthorize("@authz.check(#root, 'admin:menu')") @GetMapping("/authz") public ResponseEntity<?> authz() { return ResponseEntity.ok("自定義注解的方式"); } /** * 以下RBAC角色 + ABAC屬性的混合校驗 可以復(fù)制測試 * @PreAuthorize("hasAuthority('admin:menu') and @abacDecisionEngine.check(authentication, 'admin:menu')") * * @PreAuthorize("hasRole('ADMIN') and @authz.check(#root, 'admin:menu')") * * @return */ @PreAuthorize("hasRole('ADMIN') and @abacDecisionEngine.check(authentication, 'admin:menu')") @GetMapping("/admin/test") public ResponseEntity<?> test() { return ResponseEntity.ok("RBAC角色 + ABAC屬性的混合校驗"); } }
小伙伴們可以根據(jù)博主的代碼編寫完成后,進(jìn)行運行測試,新增用戶屬性并可以加入更多的策略來測試,如:
這里博主順便整理一些常見的策略以供大家參考:
場景描述 | SpEL表達(dá)式 |
---|---|
時間段訪問控制 | T(java.time.LocalTime).now().isBetween('09:00', '17:00') |
安全等級驗證 | attrs['securityLevel'] >= 3 && authentication.isAuthenticated() |
地理位置限制 | attrs['country'] == 'CN' && attrs['ipRegion'] == 'Shanghai' |
多因素認(rèn)證驗證 | attrs['mfaEnabled'] == true && authentication.getAuthorities().contains('MFA_VERIFIED') |
12. 完整工作流程
請求到達(dá):GET /api/admin
身份認(rèn)證:通過 UserDetailsService
加載用戶信息
屬性加載:從sys_user_attr
表獲取用戶屬性
策略匹配:查詢sys_policy
表中 target_resource
為 admin:menu
的策略
表達(dá)式評估:使用SpEL評估 #user.attrs['department'] == 'IT'
訪問決策:所有策略滿足即允許訪問
14. 總結(jié)
通過本章節(jié)相信大家對ABAC屬性權(quán)限模型的開發(fā)已經(jīng)能掌握了,值得一提的是在實際開發(fā)中,我們?yōu)榱吮苊鈹?shù)據(jù)庫壓力建議還要對用戶信息、策略信息等采用緩存處理,相關(guān)用戶屬性、決策也可以按照自身需求進(jìn)行拓展!
通過RBAC角色模型 + ABAC屬性權(quán)限模型
這種設(shè)計,你可以靈活地根據(jù)業(yè)務(wù)變化調(diào)整權(quán)限策略,實現(xiàn)更細(xì)粒度的安全控制。希望這篇實戰(zhàn)文章能夠為你的項目開發(fā)提供參考與啟發(fā)!
到此這篇關(guān)于Spring Security基于數(shù)據(jù)庫的ABAC屬性權(quán)限模型實戰(zhàn)開發(fā)教程的文章就介紹到這了,更多相關(guān)Spring Security ABAC權(quán)限模型內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java8的Stream()與ParallelStream()的區(qū)別說明
這篇文章主要介紹了Java8的Stream()與ParallelStream()的區(qū)別說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07關(guān)于Idea創(chuàng)建Java項目并引入lombok包的問題(lombok.jar包免費下載)
很多朋友遇到當(dāng)idea創(chuàng)建java項目時,命名安裝了lombok插件卻不能使用注解,原因有兩個大家可以參考下本文,本文對每種原因分析給出了解決方案,需要的朋友參考下吧2021-06-06java實現(xiàn)小型局域網(wǎng)群聊功能(C/S模式)
這篇文章主要介紹了java利用TCP協(xié)議實現(xiàn)小型局域網(wǎng)群聊功能(C/S模式) ,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-08-08Spring的自定義擴(kuò)展標(biāo)簽NamespaceHandler解析
這篇文章主要介紹了Spring的自定義擴(kuò)展標(biāo)簽NamespaceHandler解析,在很多情況下,我們需要為系統(tǒng)提供可配置化支持,簡單的做法可以直接基于Spring的標(biāo)準(zhǔn)Bean來配置,Spring提供了可擴(kuò)展Schema的支持,這是一個不錯的折中方案,需要的朋友可以參考下2023-12-12