Spring Security動態(tài)權(quán)限的實現(xiàn)方法詳解
最近在做 TienChin 項目,用的是 RuoYi-Vue 腳手架,在這個腳手架中,訪問某個接口需要什么權(quán)限,這個是在代碼中硬編碼的,具體怎么實現(xiàn)的,松哥下篇文章來和大家分析,有的小伙伴可能希望能讓這個東西像 vhr 一樣,可以在數(shù)據(jù)庫中動態(tài)配置,因此這篇文章和小伙伴們簡單介紹下 Spring Security 中的動態(tài)權(quán)限方案,以便于小伙伴們更好的理解 TienChin 項目中的權(quán)限方案。
1. 動態(tài)管理權(quán)限規(guī)則
通過代碼來配置 URL 攔截規(guī)則和請求 URL 所需要的權(quán)限,這樣就比較死板,如果想要調(diào)整訪問某一個 URL 所需要的權(quán)限,就需要修改代碼。
動態(tài)管理權(quán)限規(guī)則就是我們將 URL 攔截規(guī)則和訪問 URL 所需要的權(quán)限都保存在數(shù)據(jù)庫中,這樣,在不改變源代碼的情況下,只需要修改數(shù)據(jù)庫中的數(shù)據(jù),就可以對權(quán)限進(jìn)行調(diào)整。
1.1 數(shù)據(jù)庫設(shè)計
簡單起見,我們這里就不引入權(quán)限表了,直接使用角色表,用戶和角色關(guān)聯(lián),角色和資源關(guān)聯(lián),設(shè)計出來的表結(jié)構(gòu)如圖 13-9 所示。
圖13-9 一個簡單的權(quán)限數(shù)據(jù)庫結(jié)構(gòu)
menu 表是相當(dāng)于我們的資源表,它里邊保存了訪問規(guī)則,如圖 13-10 所示。
圖13-10 訪問規(guī)則
role 是角色表,里邊定義了系統(tǒng)中的角色,如圖 13-11 所示。
圖13-11 用戶角色表
user 是用戶表,如圖 13-12 所示。
圖13-12 用戶表
user_role 是用戶角色關(guān)聯(lián)表,用戶具有哪些角色,可以通過該表體現(xiàn)出來,如圖 13-13 所示。
圖13-13 用戶角色關(guān)聯(lián)表
menu_role 是資源角色關(guān)聯(lián)表,訪問某一個資源,需要哪些角色,可以通過該表體現(xiàn)出來,如圖 13-14 所示。
圖13-14 資源角色關(guān)聯(lián)表
至此,一個簡易的權(quán)限數(shù)據(jù)庫就設(shè)計好了(在本書提供的案例中,有SQL腳本)。
1.2 實戰(zhàn)
項目創(chuàng)建
創(chuàng)建 Spring Boot 項目,由于涉及數(shù)據(jù)庫操作,這里選用目前大家使用較多的 MyBatis 框架,所以除了引入 Web、Spring Security 依賴之外,還需要引入 MyBatis 以及 MySQL 依賴。
最終的 pom.xml 文件內(nèi)容如下:
<dependencies> ????<dependency> ????????<groupId>org.springframework.boot</groupId> ????????<artifactId>spring-boot-starter-security</artifactId> ????</dependency> ????<dependency> ????????<groupId>org.springframework.boot</groupId> ????????<artifactId>spring-boot-starter-web</artifactId> ????</dependency> ????<dependency> ????????<groupId>org.mybatis.spring.boot</groupId> ????????<artifactId>mybatis-spring-boot-starter</artifactId> ????????<version>2.1.3</version> ????</dependency> ????<dependency> ????????<groupId>mysql</groupId> ????????<artifactId>mysql-connector-java</artifactId> ????????<scope>runtime</scope> ????</dependency> </dependencies>
項目創(chuàng)建完成后,接下來在 application.properties 中配置數(shù)據(jù)庫連接信息:
spring.datasource.username=root spring.datasource.password=123 spring.datasource.url=jdbc:mysql:///security13?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
配置完成后,我們的準(zhǔn)備工作就算完成了。
創(chuàng)建實體類
根據(jù)前面設(shè)計的數(shù)據(jù)庫,我們需要創(chuàng)建三個實體類。
首先來創(chuàng)建角色類 Role:
public?class?Role?{ ????private?Integer?id; ????private?String?name; ????private?String?nameZh; ???????//省略getter/setter }
然后創(chuàng)建菜單類 Menu:
public?class?Menu?{ ????private?Integer?id; ????private?String?pattern; ????private?List<Role>?roles; ????//省略getter/setter }
菜單類中包含一個 roles 屬性,表示訪問該項資源所需要的角色。
最后我們創(chuàng)建 User 類:
public?class?User?implements?UserDetails?{ ????private?Integer?id; ????private?String?password; ????private?String?username; ????private?boolean?enabled; ????private?boolean?locked; ????private?List<Role>?roles; ????@Override ????public?Collection<??extends?GrantedAuthority>?getAuthorities()?{ ????????return?roles.stream() ????????????????????????.map(r?->?new?SimpleGrantedAuthority(r.getName())) ????????????????????????.collect(Collectors.toList()); ????} ????@Override ????public?String?getPassword()?{ ????????return?password; ????} ????@Override ????public?String?getUsername()?{ ????????return?username; ????} ????@Override ????public?boolean?isAccountNonExpired()?{ ????????return?true; ????} ????@Override ????public?boolean?isAccountNonLocked()?{ ????????return?!locked; ????} ????@Override ????public?boolean?isCredentialsNonExpired()?{ ????????return?true; ????} ????@Override ????public?boolean?isEnabled()?{ ????????return?enabled; ????} ????//省略其他getter/setter }
由于數(shù)據(jù)庫中有 enabled 和 locked 字段,所以 isEnabled() 和 isAccountNonLocked() 兩個方法如實返回,其他幾個賬戶狀態(tài)方法默認(rèn)返回 true 即可。在 getAuthorities() 方法中,我們對 roles 屬性進(jìn)行遍歷,組裝出新的集合對象返回即可。
創(chuàng)建Service
接下來我們創(chuàng)建 UserService 和 MenuService,并提供相應(yīng)的查詢方法。
先來看 UserService:
@Service public?class?UserService?implements?UserDetailsService?{ ????@Autowired ????UserMapper?userMapper; ????@Override ????public?UserDetails?loadUserByUsername(String?username)? ?????????????????????????????????????????????throws?UsernameNotFoundException?{ ????????User?user?=?userMapper.loadUserByUsername(username); ????????if?(user?==?null)?{ ????????????throw?new?UsernameNotFoundException("用戶不存在"); ????????} ????????user.setRoles(userMapper.getUserRoleByUid(user.getId())); ????????return?user; ????} }
這段代碼應(yīng)該不用多說了,不熟悉的讀者可以參考本書 2.4 節(jié)。
對應(yīng)的 UserMapper 如下:
@Mapper public?interface?UserMapper?{ ????List<Role>?getUserRoleByUid(Integer?uid); ????User?loadUserByUsername(String?username); }
UserMapper.xml:
<!DOCTYPE?mapper ????????PUBLIC?"-//mybatis.org//DTD?Mapper?3.0//EN" ????????"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper?namespace="org.javaboy.base_on_url_dy.mapper.UserMapper"> ????<select?id="loadUserByUsername"? ?????????????????????????resultType="org.javaboy.base_on_url_dy.model.User"> ????????select?*?from?user?where?username=#{username}; ????</select> ????<select?id="getUserRoleByUid"? ?????????????????????????resultType="org.javaboy.base_on_url_dy.model.Role"> ????????select?r.*?from?role?r,user_role?ur?where?ur.uid=#{uid}?and?ur.rid=r.id ????</select> </mapper>
再來看 MenuService,該類只需要提供一個方法,就是查詢出所有的 Menu 數(shù)據(jù),代碼如下:
@Service public?class?MenuService?{ ????@Autowired ????MenuMapper?menuMapper; ????public?List<Menu>?getAllMenu()?{ ????????return?menuMapper.getAllMenu(); ????} }
MenuMapper:
@Mapper public?interface?MenuMapper?{ ????List<Menu>?getAllMenu(); }
MenuMapper.xml:
<!DOCTYPE?mapper ????????PUBLIC?"-//mybatis.org//DTD?Mapper?3.0//EN" ????????"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper?namespace="org.javaboy.base_on_url_dy.mapper.MenuMapper"> ????<resultMap?id="MenuResultMap"? ????????????????????????????????type="org.javaboy.base_on_url_dy.model.Menu"> ????????<id?property="id"?column="id"/> ????????<result?property="pattern"?column="pattern"></result> ????????<collection?property="roles"? ??????????????????????????????ofType="org.javaboy.base_on_url_dy.model.Role"> ????????????<id?column="rid"?property="id"/> ????????????<result?column="rname"?property="name"/> ????????????<result?column="rnameZh"?property="nameZh"/> ????????</collection> ????</resultMap> ????<select?id="getAllMenu"?resultMap="MenuResultMap"> ????????select?m.*,r.id?as?rid,r.name?as?rname,r.nameZh?as?rnameZh?from?menu?m?left?join?menu_role?mr?on?m.`id`=mr.`mid`?left?join?role?r?on?r.`id`=mr.`rid` ????</select> </mapper>
需要注意,由于每一個 Menu 對象都包含了一個 Role 集合,所以這個查詢是一對多,這里通過 resultMap 來進(jìn)行查詢結(jié)果映射。
至此,所有基礎(chǔ)工作都完成了,接下來配置 Spring Security。
配置Spring Security
回顧 13.3.6 小節(jié)的內(nèi)容,SecurityMetadataSource 接口負(fù)責(zé)提供受保護(hù)對象所需要的權(quán)限。在本案例中,受保護(hù)對象所需要的權(quán)限保存在數(shù)據(jù)庫中,所以我們可以通過自定義類繼承自 FilterInvocationSecurityMetadataSource,并重寫 getAttributes 方法來提供受保護(hù)對象所需要的權(quán)限,代碼如下:
@Component public?class?CustomSecurityMetadataSource? ?????????????????????????implements?FilterInvocationSecurityMetadataSource?{ ????@Autowired ????MenuService?menuService; ????AntPathMatcher?antPathMatcher?=?new?AntPathMatcher(); ????@Override ????public?Collection<ConfigAttribute>?getAttributes(Object?object)? ???????????????????????????????????????????????throws?IllegalArgumentException?{ ????????String?requestURI?=? ???????????????????((FilterInvocation)?object).getRequest().getRequestURI(); ????????List<Menu>?allMenu?=?menuService.getAllMenu(); ????????for?(Menu?menu?:?allMenu)?{ ????????????if?(antPathMatcher.match(menu.getPattern(),?requestURI))?{ ????????????????String[]?roles?=?menu.getRoles().stream() ???????????????????????????????.map(r?->?r.getName()).toArray(String[]::new); ????????????????return?SecurityConfig.createList(roles); ????????????} ????????} ????????return?null; ????} ????@Override ????public?Collection<ConfigAttribute>?getAllConfigAttributes()?{ ????????return?null; ????} ????@Override ????public?boolean?supports(Class<?>?clazz)?{ ????????return?FilterInvocation.class.isAssignableFrom(clazz); ????} }
自定義 CustomSecurityMetadataSource 類并實現(xiàn) FilterInvocationSecurityMetadataSource 接口,然后重寫它里邊的三個方法:
- getAttributes:該方法的參數(shù)是受保護(hù)對象,在基于 URL 地址的權(quán)限控制中,受保護(hù)對象就是 FilterInvocation;該方法的返回值則是訪問受保護(hù)對象所需要的權(quán)限。在該方法里邊,我們首先從受保護(hù)對象 FilterInvocation 中提取出當(dāng)前請求的 URL 地址,例如
/admin/hello
,然后通過 menuService 對象查詢出所有的菜單數(shù)據(jù)(每條數(shù)據(jù)中都包含訪問該條記錄所需要的權(quán)限),遍歷查詢出來的菜單數(shù)據(jù),如果當(dāng)前請求的 URL 地址和菜單中某一條記錄的 pattern 屬性匹配上了(例如/admin/hello
匹配上/admin/**
),那么我們就可以獲取當(dāng)前請求所需要的權(quán)限。從 menu 對象中獲取 roles 屬性,并將其轉(zhuǎn)為一個數(shù)組,然后通過SecurityConfig.createList
方法創(chuàng)建一個Collection<ConfigAttribute>
對象并返回。如果當(dāng)前請求的 URL 地址和數(shù)據(jù)庫中 menu 表的所有項都匹配不上,那么最終返回 null。如果返回 null,那么受保護(hù)對象到底能不能訪問呢?這就要看 AbstractSecurityInterceptor 對象中的 rejectPublicInvocations 屬性了,該屬性默認(rèn)為 false,表示當(dāng) getAttributes 方法返回 null 時,允許訪問受保護(hù)對象(回顧 13.4.4 小節(jié)中關(guān)于AbstractSecurityInterceptor#beforeInvocation
的講解)。 - getAllConfigAttributes:該方法可以用來返回所有的權(quán)限屬性,以便在項目啟動階段做校驗,如果不需要校驗,則直接返回 null 即可。
- supports:該方法表示當(dāng)前對象支持處理的受保護(hù)對象是 FilterInvocation。
CustomSecurityMetadataSource
類配置完成后,接下來我們要用它來代替默認(rèn)的 SecurityMetadataSource
對象,具體配置如下:
@Configuration public?class?SecurityConfig?extends?WebSecurityConfigurerAdapter?{ ????@Autowired ????CustomSecurityMetadataSource?customSecurityMetadataSource; ????@Autowired ????UserService?userService; ????@Override ????protected?void?configure(AuthenticationManagerBuilder?auth)? ????????????????????????????????????????????????????????????????throws?Exception?{ ????????auth.userDetailsService(userService); ????} ????@Override ????protected?void?configure(HttpSecurity?http)?throws?Exception?{ ????????ApplicationContext?applicationContext?=? ??????????????????????????????http.getSharedObject(ApplicationContext.class); ????????http.apply(new?UrlAuthorizationConfigurer<>(applicationContext)) ????????????????.withObjectPostProcessor(new? ???????????????????????????ObjectPostProcessor<FilterSecurityInterceptor>()?{ ????????????????????@Override ????????????????????public?<O?extends?FilterSecurityInterceptor>?O? ????????????????????????????????????????????????????????????postProcess(O?object)?{ ???????????object.setSecurityMetadataSource(customSecurityMetadataSource); ????????????????????????return?object; ????????????????????} ????????????????}); ????????http.formLogin() ????????????????.and() ????????????????.csrf().disable(); ????} }
關(guān)于用戶的配置無需多說,我們重點來看 configure(HttpSecurity) 方法。
由于訪問路徑規(guī)則和所需要的權(quán)限之間的映射關(guān)系已經(jīng)保存在數(shù)據(jù)庫中,所以我們就沒有必要在 Java 代碼中配置映射關(guān)系了,同時這里的權(quán)限對比也不會用到權(quán)限表達(dá)式,所以我們通過 UrlAuthorizationConfigurer 來進(jìn)行配置。
在配置的過程中,通過 withObjectPostProcessor 方法調(diào)用 ObjectPostProcessor 對象后置處理器,在對象后置處理器中,將 FilterSecurityInterceptor 中的 SecurityMetadataSource 對象替換為我們自定義的 customSecurityMetadataSource 對象即可。
2. 測試
接下來創(chuàng)建 HelloController,代碼如下:
@RestController public?class?HelloController?{ ????@GetMapping("/admin/hello") ????public?String?admin()?{ ????????return?"hello?admin"; ????} ????@GetMapping("/user/hello") ????public?String?user()?{ ????????return?"hello?user"; ????} ????@GetMapping("/guest/hello") ????public?String?guest()?{ ????????return?"hello?guest"; ????} ????@GetMapping("/hello") ????public?String?hello()?{ ????????return?"hello"; ????} }
最后啟動項目進(jìn)行測試。
首先使用 admin/123
進(jìn)行登錄,該用戶具備 ROLE_ADMIN
角色,ROLE_ADMIN
可以訪問 /admin/hello
、/user/hello
以及 /guest/hello
三個接口。
接下來使用 user/123
進(jìn)行登錄,該用戶具備 ROLE_USER
角色,ROLE_USER
可以訪問 /user/hello
以及 /guest/hello
兩個接口。
最后使用 javaboy/123
進(jìn)行登錄,該用戶具備 ROLE_GUEST
角色,ROLE_GUEST
可以訪問 /guest/hello
接口。
由于 /hello
接口不包含在 URL-權(quán)限
映射關(guān)系中,所以任何用戶都可以訪問 /hello
接口,包括匿名用戶。如果希望所有的 URL
地址都必須在數(shù)據(jù)庫中配置 URL-權(quán)限
映射關(guān)系后才能訪問,那么可以通過如下配置實現(xiàn):
http.apply(new?UrlAuthorizationConfigurer<>(applicationContext)) ????????.withObjectPostProcessor(new?? ???????????????????????????ObjectPostProcessor<FilterSecurityInterceptor>()?{ ????????????@Override ????????????public?<O?extends?FilterSecurityInterceptor>?O? ???????????????????????????????????????????????????????????postProcess(O?object)?{??? ???????????object.setSecurityMetadataSource(customSecurityMetadataSource); ????????????????object.setRejectPublicInvocations(true); ????????????????return?object; ????????????} ????????});
通過設(shè)置 FilterSecurityInterceptor 中的 rejectPublicInvocations 屬性為 true,就可以關(guān)閉URL的公開訪問,所有 URL 必須具備對應(yīng)的權(quán)限才能訪問。
以上就是Spring Security動態(tài)權(quán)限的實現(xiàn)方法詳解的詳細(xì)內(nèi)容,更多關(guān)于Spring Security動態(tài)權(quán)限的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
關(guān)于MyBatis模糊查詢的幾種實現(xiàn)方式
在實際項目中,我們會經(jīng)常對數(shù)據(jù)做一些模糊查詢的操作,這時候就需要利用到 like字段,那么在Mybatis中,有哪些方式可以實現(xiàn)模糊查詢呢,需要的朋友可以參考下2023-05-05Java中的Gradle與Groovy的區(qū)別及存在的關(guān)系
這篇文章主要介紹了Java中的Gradle與Groovy的區(qū)別及存在的關(guān)系,Groovy是一種JVM語言,它可以編譯為與Java相同的字節(jié)碼,并且可以與Java類無縫地互操作,Gradle是Java項目中主要的構(gòu)建系統(tǒng)之一,下文關(guān)于兩者的詳細(xì)內(nèi)容,需要的小伙伴可以參考一下2022-02-02詳解 Corba開發(fā)之Java實現(xiàn)Service與Client
這篇文章主要介紹了詳解 Corba開發(fā)之Java實現(xiàn)Service與Client的相關(guān)資料,希望通過本文能幫助到大家,需要的朋友可以參考下2017-10-10Java中的布隆過濾器原理實現(xiàn)和應(yīng)用
Java中的布隆過濾器是一種基于哈希函數(shù)的數(shù)據(jù)結(jié)構(gòu),能夠高效地判斷元素是否存在于一個集合中。它廣泛應(yīng)用于緩存、網(wǎng)絡(luò)協(xié)議、數(shù)據(jù)查詢等領(lǐng)域,在提高程序性能和減少資源消耗方面具有顯著優(yōu)勢2023-04-04SpringBoot全局異常與數(shù)據(jù)校驗的方法
這篇文章主要介紹了SpringBoot全局異常與數(shù)據(jù)校驗的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-11-11SpringBoot+Redis實現(xiàn)數(shù)據(jù)字典的方法
這篇文章主要介紹了SpringBoot+Redis實現(xiàn)數(shù)據(jù)字典的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10springboot?aop配合反射統(tǒng)一簽名驗證實踐
這篇文章主要介紹了springboot?aop配合反射統(tǒng)一簽名驗證實踐,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12