Spring Security動(dòng)態(tài)權(quán)限的實(shí)現(xiàn)方法詳解
最近在做 TienChin 項(xiàng)目,用的是 RuoYi-Vue 腳手架,在這個(gè)腳手架中,訪問某個(gè)接口需要什么權(quán)限,這個(gè)是在代碼中硬編碼的,具體怎么實(shí)現(xiàn)的,松哥下篇文章來和大家分析,有的小伙伴可能希望能讓這個(gè)東西像 vhr 一樣,可以在數(shù)據(jù)庫(kù)中動(dòng)態(tài)配置,因此這篇文章和小伙伴們簡(jiǎn)單介紹下 Spring Security 中的動(dòng)態(tài)權(quán)限方案,以便于小伙伴們更好的理解 TienChin 項(xiàng)目中的權(quán)限方案。
1. 動(dòng)態(tài)管理權(quán)限規(guī)則
通過代碼來配置 URL 攔截規(guī)則和請(qǐng)求 URL 所需要的權(quán)限,這樣就比較死板,如果想要調(diào)整訪問某一個(gè) URL 所需要的權(quán)限,就需要修改代碼。
動(dòng)態(tài)管理權(quán)限規(guī)則就是我們將 URL 攔截規(guī)則和訪問 URL 所需要的權(quán)限都保存在數(shù)據(jù)庫(kù)中,這樣,在不改變?cè)创a的情況下,只需要修改數(shù)據(jù)庫(kù)中的數(shù)據(jù),就可以對(duì)權(quán)限進(jìn)行調(diào)整。
1.1 數(shù)據(jù)庫(kù)設(shè)計(jì)
簡(jiǎn)單起見,我們這里就不引入權(quán)限表了,直接使用角色表,用戶和角色關(guān)聯(lián),角色和資源關(guān)聯(lián),設(shè)計(jì)出來的表結(jié)構(gòu)如圖 13-9 所示。
圖13-9 一個(gè)簡(jiǎn)單的權(quán)限數(shù)據(jù)庫(kù)結(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)表,訪問某一個(gè)資源,需要哪些角色,可以通過該表體現(xiàn)出來,如圖 13-14 所示。
圖13-14 資源角色關(guān)聯(lián)表
至此,一個(gè)簡(jiǎn)易的權(quán)限數(shù)據(jù)庫(kù)就設(shè)計(jì)好了(在本書提供的案例中,有SQL腳本)。
1.2 實(shí)戰(zhàn)
項(xiàng)目創(chuàng)建
創(chuàng)建 Spring Boot 項(xiàng)目,由于涉及數(shù)據(jù)庫(kù)操作,這里選用目前大家使用較多的 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>
項(xiàng)目創(chuàng)建完成后,接下來在 application.properties 中配置數(shù)據(jù)庫(kù)連接信息:
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)建實(shí)體類
根據(jù)前面設(shè)計(jì)的數(shù)據(jù)庫(kù),我們需要?jiǎng)?chuàng)建三個(gè)實(shí)體類。
首先來創(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 }
菜單類中包含一個(gè) roles 屬性,表示訪問該項(xiàng)資源所需要的角色。
最后我們創(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ù)庫(kù)中有 enabled 和 locked 字段,所以 isEnabled() 和 isAccountNonLocked() 兩個(gè)方法如實(shí)返回,其他幾個(gè)賬戶狀態(tài)方法默認(rèn)返回 true 即可。在 getAuthorities() 方法中,我們對(duì) roles 屬性進(jìn)行遍歷,組裝出新的集合對(duì)象返回即可。
創(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é)。
對(duì)應(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,該類只需要提供一個(gè)方法,就是查詢出所有的 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>
需要注意,由于每一個(gè) Menu 對(duì)象都包含了一個(gè) Role 集合,所以這個(gè)查詢是一對(duì)多,這里通過 resultMap 來進(jìn)行查詢結(jié)果映射。
至此,所有基礎(chǔ)工作都完成了,接下來配置 Spring Security。
配置Spring Security
回顧 13.3.6 小節(jié)的內(nèi)容,SecurityMetadataSource 接口負(fù)責(zé)提供受保護(hù)對(duì)象所需要的權(quán)限。在本案例中,受保護(hù)對(duì)象所需要的權(quán)限保存在數(shù)據(jù)庫(kù)中,所以我們可以通過自定義類繼承自 FilterInvocationSecurityMetadataSource,并重寫 getAttributes 方法來提供受保護(hù)對(duì)象所需要的權(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 類并實(shí)現(xiàn) FilterInvocationSecurityMetadataSource 接口,然后重寫它里邊的三個(gè)方法:
- getAttributes:該方法的參數(shù)是受保護(hù)對(duì)象,在基于 URL 地址的權(quán)限控制中,受保護(hù)對(duì)象就是 FilterInvocation;該方法的返回值則是訪問受保護(hù)對(duì)象所需要的權(quán)限。在該方法里邊,我們首先從受保護(hù)對(duì)象 FilterInvocation 中提取出當(dāng)前請(qǐng)求的 URL 地址,例如
/admin/hello
,然后通過 menuService 對(duì)象查詢出所有的菜單數(shù)據(jù)(每條數(shù)據(jù)中都包含訪問該條記錄所需要的權(quán)限),遍歷查詢出來的菜單數(shù)據(jù),如果當(dāng)前請(qǐng)求的 URL 地址和菜單中某一條記錄的 pattern 屬性匹配上了(例如/admin/hello
匹配上/admin/**
),那么我們就可以獲取當(dāng)前請(qǐng)求所需要的權(quán)限。從 menu 對(duì)象中獲取 roles 屬性,并將其轉(zhuǎn)為一個(gè)數(shù)組,然后通過SecurityConfig.createList
方法創(chuàng)建一個(gè)Collection<ConfigAttribute>
對(duì)象并返回。如果當(dāng)前請(qǐng)求的 URL 地址和數(shù)據(jù)庫(kù)中 menu 表的所有項(xiàng)都匹配不上,那么最終返回 null。如果返回 null,那么受保護(hù)對(duì)象到底能不能訪問呢?這就要看 AbstractSecurityInterceptor 對(duì)象中的 rejectPublicInvocations 屬性了,該屬性默認(rèn)為 false,表示當(dāng) getAttributes 方法返回 null 時(shí),允許訪問受保護(hù)對(duì)象(回顧 13.4.4 小節(jié)中關(guān)于AbstractSecurityInterceptor#beforeInvocation
的講解)。 - getAllConfigAttributes:該方法可以用來返回所有的權(quán)限屬性,以便在項(xiàng)目啟動(dòng)階段做校驗(yàn),如果不需要校驗(yàn),則直接返回 null 即可。
- supports:該方法表示當(dāng)前對(duì)象支持處理的受保護(hù)對(duì)象是 FilterInvocation。
CustomSecurityMetadataSource
類配置完成后,接下來我們要用它來代替默認(rèn)的 SecurityMetadataSource
對(duì)象,具體配置如下:
@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)于用戶的配置無需多說,我們重點(diǎn)來看 configure(HttpSecurity) 方法。
由于訪問路徑規(guī)則和所需要的權(quán)限之間的映射關(guān)系已經(jīng)保存在數(shù)據(jù)庫(kù)中,所以我們就沒有必要在 Java 代碼中配置映射關(guān)系了,同時(shí)這里的權(quán)限對(duì)比也不會(huì)用到權(quán)限表達(dá)式,所以我們通過 UrlAuthorizationConfigurer 來進(jìn)行配置。
在配置的過程中,通過 withObjectPostProcessor 方法調(diào)用 ObjectPostProcessor 對(duì)象后置處理器,在對(duì)象后置處理器中,將 FilterSecurityInterceptor 中的 SecurityMetadataSource 對(duì)象替換為我們自定義的 customSecurityMetadataSource 對(duì)象即可。
2. 測(cè)試
接下來創(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"; ????} }
最后啟動(dòng)項(xiàng)目進(jìn)行測(cè)試。
首先使用 admin/123
進(jìn)行登錄,該用戶具備 ROLE_ADMIN
角色,ROLE_ADMIN
可以訪問 /admin/hello
、/user/hello
以及 /guest/hello
三個(gè)接口。
接下來使用 user/123
進(jìn)行登錄,該用戶具備 ROLE_USER
角色,ROLE_USER
可以訪問 /user/hello
以及 /guest/hello
兩個(gè)接口。
最后使用 javaboy/123
進(jìn)行登錄,該用戶具備 ROLE_GUEST
角色,ROLE_GUEST
可以訪問 /guest/hello
接口。
由于 /hello
接口不包含在 URL-權(quán)限
映射關(guān)系中,所以任何用戶都可以訪問 /hello
接口,包括匿名用戶。如果希望所有的 URL
地址都必須在數(shù)據(jù)庫(kù)中配置 URL-權(quán)限
映射關(guān)系后才能訪問,那么可以通過如下配置實(shí)現(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 必須具備對(duì)應(yīng)的權(quán)限才能訪問。
以上就是Spring Security動(dòng)態(tài)權(quán)限的實(shí)現(xiàn)方法詳解的詳細(xì)內(nèi)容,更多關(guān)于Spring Security動(dòng)態(tài)權(quán)限的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
關(guān)于MyBatis模糊查詢的幾種實(shí)現(xiàn)方式
在實(shí)際項(xiàng)目中,我們會(huì)經(jīng)常對(duì)數(shù)據(jù)做一些模糊查詢的操作,這時(shí)候就需要利用到 like字段,那么在Mybatis中,有哪些方式可以實(shí)現(xiàn)模糊查詢呢,需要的朋友可以參考下2023-05-05Java中的Gradle與Groovy的區(qū)別及存在的關(guān)系
這篇文章主要介紹了Java中的Gradle與Groovy的區(qū)別及存在的關(guān)系,Groovy是一種JVM語言,它可以編譯為與Java相同的字節(jié)碼,并且可以與Java類無縫地互操作,Gradle是Java項(xiàng)目中主要的構(gòu)建系統(tǒng)之一,下文關(guān)于兩者的詳細(xì)內(nèi)容,需要的小伙伴可以參考一下2022-02-02java實(shí)現(xiàn)題目以及選項(xiàng)亂序的方法實(shí)例
這篇文章主要給大家介紹了關(guān)于java實(shí)現(xiàn)題目以及選項(xiàng)亂序的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03詳解 Corba開發(fā)之Java實(shí)現(xiàn)Service與Client
這篇文章主要介紹了詳解 Corba開發(fā)之Java實(shí)現(xiàn)Service與Client的相關(guān)資料,希望通過本文能幫助到大家,需要的朋友可以參考下2017-10-10Java中的布隆過濾器原理實(shí)現(xiàn)和應(yīng)用
Java中的布隆過濾器是一種基于哈希函數(shù)的數(shù)據(jù)結(jié)構(gòu),能夠高效地判斷元素是否存在于一個(gè)集合中。它廣泛應(yīng)用于緩存、網(wǎng)絡(luò)協(xié)議、數(shù)據(jù)查詢等領(lǐng)域,在提高程序性能和減少資源消耗方面具有顯著優(yōu)勢(shì)2023-04-04SpringBoot全局異常與數(shù)據(jù)校驗(yàn)的方法
這篇文章主要介紹了SpringBoot全局異常與數(shù)據(jù)校驗(yàn)的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-11-11SpringBoot+Redis實(shí)現(xiàn)數(shù)據(jù)字典的方法
這篇文章主要介紹了SpringBoot+Redis實(shí)現(xiàn)數(shù)據(jù)字典的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10springboot?aop配合反射統(tǒng)一簽名驗(yàn)證實(shí)踐
這篇文章主要介紹了springboot?aop配合反射統(tǒng)一簽名驗(yàn)證實(shí)踐,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12