Spring Security動(dòng)態(tài)權(quán)限的實(shí)現(xiàn)方法詳解
最近在做 TienChin 項(xiàng)目,用的是 RuoYi-Vue 腳手架,在這個(gè)腳手架中,訪問(wèn)某個(gè)接口需要什么權(quán)限,這個(gè)是在代碼中硬編碼的,具體怎么實(shí)現(xiàn)的,松哥下篇文章來(lái)和大家分析,有的小伙伴可能希望能讓這個(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ī)則
通過(guò)代碼來(lái)配置 URL 攔截規(guī)則和請(qǐng)求 URL 所需要的權(quán)限,這樣就比較死板,如果想要調(diào)整訪問(wèn)某一個(gè) URL 所需要的權(quán)限,就需要修改代碼。
動(dòng)態(tài)管理權(quán)限規(guī)則就是我們將 URL 攔截規(guī)則和訪問(wèn) 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)單起見(jiàn),我們這里就不引入權(quán)限表了,直接使用角色表,用戶和角色關(guān)聯(lián),角色和資源關(guān)聯(lián),設(shè)計(jì)出來(lái)的表結(jié)構(gòu)如圖 13-9 所示。

圖13-9 一個(gè)簡(jiǎn)單的權(quán)限數(shù)據(jù)庫(kù)結(jié)構(gòu)
menu 表是相當(dāng)于我們的資源表,它里邊保存了訪問(wèn)規(guī)則,如圖 13-10 所示。

圖13-10 訪問(wèn)規(guī)則
role 是角色表,里邊定義了系統(tǒng)中的角色,如圖 13-11 所示。

圖13-11 用戶角色表
user 是用戶表,如圖 13-12 所示。

圖13-12 用戶表
user_role 是用戶角色關(guān)聯(lián)表,用戶具有哪些角色,可以通過(guò)該表體現(xiàn)出來(lái),如圖 13-13 所示。

圖13-13 用戶角色關(guān)聯(lián)表
menu_role 是資源角色關(guān)聯(lián)表,訪問(wèn)某一個(gè)資源,需要哪些角色,可以通過(guò)該表體現(xiàn)出來(lái),如圖 13-14 所示。

圖13-14 資源角色關(guān)聯(lián)表
至此,一個(gè)簡(jiǎn)易的權(quán)限數(shù)據(jù)庫(kù)就設(shè)計(jì)好了(在本書(shū)提供的案例中,有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)建完成后,接下來(lái)在 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í)體類(lèi)
根據(jù)前面設(shè)計(jì)的數(shù)據(jù)庫(kù),我們需要?jiǎng)?chuàng)建三個(gè)實(shí)體類(lèi)。
首先來(lái)創(chuàng)建角色類(lèi) Role:
public?class?Role?{
????private?Integer?id;
????private?String?name;
????private?String?nameZh;
???????//省略getter/setter
}
然后創(chuàng)建菜單類(lèi) Menu:
public?class?Menu?{
????private?Integer?id;
????private?String?pattern;
????private?List<Role>?roles;
????//省略getter/setter
}
菜單類(lèi)中包含一個(gè) roles 屬性,表示訪問(wèn)該項(xiàng)資源所需要的角色。
最后我們創(chuàng)建 User 類(lèi):
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
接下來(lái)我們創(chuàng)建 UserService 和 MenuService,并提供相應(yīng)的查詢方法。
先來(lái)看 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)該不用多說(shuō)了,不熟悉的讀者可以參考本書(shū) 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>
再來(lái)看 MenuService,該類(lèi)只需要提供一個(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ì)多,這里通過(guò) resultMap 來(lái)進(jìn)行查詢結(jié)果映射。
至此,所有基礎(chǔ)工作都完成了,接下來(lái)配置 Spring Security。
配置Spring Security
回顧 13.3.6 小節(jié)的內(nèi)容,SecurityMetadataSource 接口負(fù)責(zé)提供受保護(hù)對(duì)象所需要的權(quán)限。在本案例中,受保護(hù)對(duì)象所需要的權(quán)限保存在數(shù)據(jù)庫(kù)中,所以我們可以通過(guò)自定義類(lèi)繼承自 FilterInvocationSecurityMetadataSource,并重寫(xiě) getAttributes 方法來(lái)提供受保護(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 類(lèi)并實(shí)現(xiàn) FilterInvocationSecurityMetadataSource 接口,然后重寫(xiě)它里邊的三個(gè)方法:
- getAttributes:該方法的參數(shù)是受保護(hù)對(duì)象,在基于 URL 地址的權(quán)限控制中,受保護(hù)對(duì)象就是 FilterInvocation;該方法的返回值則是訪問(wèn)受保護(hù)對(duì)象所需要的權(quán)限。在該方法里邊,我們首先從受保護(hù)對(duì)象 FilterInvocation 中提取出當(dāng)前請(qǐng)求的 URL 地址,例如
/admin/hello,然后通過(guò) menuService 對(duì)象查詢出所有的菜單數(shù)據(jù)(每條數(shù)據(jù)中都包含訪問(wèn)該條記錄所需要的權(quán)限),遍歷查詢出來(lái)的菜單數(shù)據(jù),如果當(dāng)前請(qǐng)求的 URL 地址和菜單中某一條記錄的 pattern 屬性匹配上了(例如/admin/hello匹配上/admin/**),那么我們就可以獲取當(dāng)前請(qǐng)求所需要的權(quán)限。從 menu 對(duì)象中獲取 roles 屬性,并將其轉(zhuǎn)為一個(gè)數(shù)組,然后通過(guò)SecurityConfig.createList方法創(chuàng)建一個(gè)Collection<ConfigAttribute>對(duì)象并返回。如果當(dāng)前請(qǐng)求的 URL 地址和數(shù)據(jù)庫(kù)中 menu 表的所有項(xiàng)都匹配不上,那么最終返回 null。如果返回 null,那么受保護(hù)對(duì)象到底能不能訪問(wèn)呢?這就要看 AbstractSecurityInterceptor 對(duì)象中的 rejectPublicInvocations 屬性了,該屬性默認(rèn)為 false,表示當(dāng) getAttributes 方法返回 null 時(shí),允許訪問(wèn)受保護(hù)對(duì)象(回顧 13.4.4 小節(jié)中關(guān)于AbstractSecurityInterceptor#beforeInvocation的講解)。 - getAllConfigAttributes:該方法可以用來(lái)返回所有的權(quán)限屬性,以便在項(xiàng)目啟動(dòng)階段做校驗(yàn),如果不需要校驗(yàn),則直接返回 null 即可。
- supports:該方法表示當(dāng)前對(duì)象支持處理的受保護(hù)對(duì)象是 FilterInvocation。
CustomSecurityMetadataSource 類(lèi)配置完成后,接下來(lái)我們要用它來(lái)代替默認(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)于用戶的配置無(wú)需多說(shuō),我們重點(diǎn)來(lái)看 configure(HttpSecurity) 方法。
由于訪問(wèn)路徑規(guī)則和所需要的權(quán)限之間的映射關(guān)系已經(jīng)保存在數(shù)據(jù)庫(kù)中,所以我們就沒(méi)有必要在 Java 代碼中配置映射關(guān)系了,同時(shí)這里的權(quán)限對(duì)比也不會(huì)用到權(quán)限表達(dá)式,所以我們通過(guò) UrlAuthorizationConfigurer 來(lái)進(jìn)行配置。
在配置的過(guò)程中,通過(guò) withObjectPostProcessor 方法調(diào)用 ObjectPostProcessor 對(duì)象后置處理器,在對(duì)象后置處理器中,將 FilterSecurityInterceptor 中的 SecurityMetadataSource 對(duì)象替換為我們自定義的 customSecurityMetadataSource 對(duì)象即可。
2. 測(cè)試
接下來(lái)創(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 可以訪問(wèn) /admin/hello、/user/hello 以及 /guest/hello 三個(gè)接口。
接下來(lái)使用 user/123 進(jìn)行登錄,該用戶具備 ROLE_USER 角色,ROLE_USER 可以訪問(wèn) /user/hello 以及 /guest/hello 兩個(gè)接口。
最后使用 javaboy/123 進(jìn)行登錄,該用戶具備 ROLE_GUEST 角色,ROLE_GUEST 可以訪問(wèn) /guest/hello 接口。
由于 /hello 接口不包含在 URL-權(quán)限 映射關(guān)系中,所以任何用戶都可以訪問(wèn) /hello 接口,包括匿名用戶。如果希望所有的 URL 地址都必須在數(shù)據(jù)庫(kù)中配置 URL-權(quán)限 映射關(guān)系后才能訪問(wèn),那么可以通過(guò)如下配置實(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;
????????????}
????????});
通過(guò)設(shè)置 FilterSecurityInterceptor 中的 rejectPublicInvocations 屬性為 true,就可以關(guān)閉URL的公開(kāi)訪問(wèn),所有 URL 必須具備對(duì)應(yīng)的權(quán)限才能訪問(wè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)文章!
- 詳解Spring Security 中的四種權(quán)限控制方式
- java中自定義Spring Security權(quán)限控制管理示例(實(shí)戰(zhàn)篇)
- spring security動(dòng)態(tài)配置url權(quán)限的2種實(shí)現(xiàn)方法
- SpringSecurity動(dòng)態(tài)加載用戶角色權(quán)限實(shí)現(xiàn)登錄及鑒權(quán)功能
- Spring security實(shí)現(xiàn)登陸和權(quán)限角色控制
- 解決Spring Security的權(quán)限配置不生效問(wèn)題
- SpringBoot整合Security實(shí)現(xiàn)權(quán)限控制框架(案例詳解)
- Spring security實(shí)現(xiàn)權(quán)限管理示例
- SpringBoot2.0 整合 SpringSecurity 框架實(shí)現(xiàn)用戶權(quán)限安全管理方法
- 基于Spring Security的動(dòng)態(tài)權(quán)限系統(tǒng)設(shè)計(jì)與實(shí)現(xiàn)
相關(guān)文章
詳解SpringMVC?HandlerInterceptor攔截器的使用與參數(shù)
本文主要介紹了詳解SpringMVC?HandlerInterceptor攔截器的使用與參數(shù),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01
在SpringBoot項(xiàng)目中的使用Swagger的方法示例
這篇文章主要介紹了在SpringBoot項(xiàng)目中的使用Swagger的方法示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05
Windows下Java+MyBatis框架+MySQL的開(kāi)發(fā)環(huán)境搭建教程
這篇文章主要介紹了Windows下Java+MyBatis框架+MySQL的開(kāi)發(fā)環(huán)境搭建教程,Mybatis對(duì)普通SQL語(yǔ)句的支持非常好,需要的朋友可以參考下2016-04-04
在Java中避免NullPointerException的解決方案
這篇文章主要介紹了在Java中避免NullPointerException的解決方案,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04
SpringBoot接口數(shù)據(jù)加解密實(shí)戰(zhàn)記錄
現(xiàn)今對(duì)于大多數(shù)公司來(lái)說(shuō),信息安全工作尤為重要,下面這篇文章主要給大家介紹了關(guān)于SpringBoot接口數(shù)據(jù)加解密的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07
Java中具有映射關(guān)系的容器:數(shù)組和Map的區(qū)別說(shuō)明
這篇文章主要介紹了Java中具有映射關(guān)系的容器:數(shù)組和Map的區(qū)別說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09
Spring Cloud Alibaba Nacos 入門(mén)詳解
這篇文章主要介紹了Spring Cloud Alibaba Nacos入門(mén)詳解,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-03-03
關(guān)于maven使用過(guò)程中無(wú)法導(dǎo)入依賴的一些總結(jié)
這篇文章主要介紹了關(guān)于maven使用過(guò)程中無(wú)法導(dǎo)入依賴的一些總結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08
idea上提交項(xiàng)目到gitee 最后出現(xiàn) Push rejected的問(wèn)題處理方法
這篇文章主要介紹了idea上面提交項(xiàng)目到gitee 最后出現(xiàn) Push rejected的問(wèn)題處理方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09

