詳解Spring Security如何在權(quán)限中使用通配符
前言
小伙伴們知道,在 Shiro 中,默認(rèn)是支持權(quán)限通配符的,例如系統(tǒng)用戶有如下一些權(quán)限:
- system:user:add
- system:user:delete
- system:user:select
- system:user:update
- …
現(xiàn)在給用戶授權(quán)的時(shí)候,我們可以像上面這樣,一個(gè)權(quán)限一個(gè)權(quán)限的配置,也可以直接用通配符:
system:user:*
這個(gè)通配符就表示擁有針對(duì)用戶的所有權(quán)限。
今天我們來聊聊 Spring Security 中對(duì)此如何處理,也順便來看看 TienChin 項(xiàng)目中,這塊該如何改進(jìn)。
1. SpEL
要搞明白基于注解的權(quán)限管理,那么得首先理解 SpEL,不需要了解多深入,我這里就簡(jiǎn)單介紹下。
Spring Expression Language(簡(jiǎn)稱 SpEL)是一個(gè)支持查詢和操作運(yùn)行時(shí)對(duì)象導(dǎo)航圖功能的強(qiáng)大的表達(dá)式語言。它的語法類似于傳統(tǒng) EL,但提供額外的功能,最出色的就是函數(shù)調(diào)用和簡(jiǎn)單字符串的模板函數(shù)。
SpEL 給 Spring 社區(qū)提供一種簡(jiǎn)單而高效的表達(dá)式語言,一種可貫穿整個(gè) Spring 產(chǎn)品組的語言。這種語言的特性基于 Spring 產(chǎn)品的需求而設(shè)計(jì),這是它出現(xiàn)的一大特色。
在我們離不開 Spring 框架的同時(shí),其實(shí)我們也已經(jīng)離不開 SpEL 了,因?yàn)樗糜?、太?qiáng)大了,SpEL 在整個(gè) Spring 家族中也處于一個(gè)非常重要的位置。但是很多時(shí)候,我們對(duì)它的只了解一個(gè)大概,其實(shí)如果你系統(tǒng)的學(xué)習(xí)過 SpEL,那么上面 Spring Security 那個(gè)注解其實(shí)很好理解。
我先通過一個(gè)簡(jiǎn)單的例子來和大家捋一捋 SpEL。
為了省事,我就創(chuàng)建一個(gè) Spring Boot 工程來和大家演示,創(chuàng)建的時(shí)候不用加任何額外的依賴,就最最基礎(chǔ)的依賴即可。
代碼如下:
String expressionStr = "1 + 2"; ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression(expressionStr);
expressionStr 是我們自定義的一個(gè)表達(dá)式字符串,這個(gè)字符串通過一個(gè) ExpressionParser 對(duì)象將之解析為一個(gè) Expression,接下來就可以執(zhí)行這個(gè) exp 了。
執(zhí)行的時(shí)候有兩種方式,對(duì)于我們上面這種不帶任何額外變量的,我們可以直接執(zhí)行,直接執(zhí)行的方式如下:
Object value = exp.getValue(); System.out.println(value.toString());
這個(gè)打印結(jié)果為 3。
我記得之前有個(gè)小伙伴在群里問想執(zhí)行一個(gè)字符串表達(dá)式,但是不知道怎么辦,js 中有 eval 函數(shù)很方便,我們 Java 中也有 SpEL,一樣也很方便。
不過很多時(shí)候,我們要執(zhí)行的表達(dá)式可能比較復(fù)雜,這時(shí)候上面這種調(diào)用方式就不太夠用了。
此時(shí)我們可以為要調(diào)用的表達(dá)式設(shè)置一個(gè)上下文環(huán)境,這個(gè)時(shí)候就會(huì)用到 EvaluationContext 或者它的子類,如下:
StandardEvaluationContext context = new StandardEvaluationContext(); System.out.println(exp.getValue(context));
當(dāng)然上面這個(gè)表達(dá)式不需要設(shè)置上下文環(huán)境,我舉一個(gè)需要設(shè)置上下文環(huán)境的例子。
例如我現(xiàn)在有一個(gè) User 類,如下:
public class User {
private Integer id;
private String username;
private String address;
//省略 getter/setter
}
現(xiàn)在我的表達(dá)式是這樣:
String expression = "#user.username";
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(expression);
StandardEvaluationContext ctx = new StandardEvaluationContext();
User user = new User();
user.setAddress("廣州");
user.setUsername("javaboy");
user.setId(99);
ctx.setVariable("user", user);
String value = exp.getValue(ctx, String.class);
System.out.println("value = " + value);
這個(gè)表達(dá)式就表示獲取 user 對(duì)象的 username 屬性。將來創(chuàng)建一個(gè) user 對(duì)象,放到 StandardEvaluationContext 中,并基于此對(duì)象執(zhí)行表達(dá)式,就可以打印出來想要的結(jié)果。
如果我們將 user 對(duì)象設(shè)置為 rootObject,那么表達(dá)式中就不需要 user 了,如下:
String expression = "username";
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(expression);
StandardEvaluationContext ctx = new StandardEvaluationContext();
User user = new User();
user.setAddress("廣州");
user.setUsername("javaboy");
user.setId(99);
ctx.setRootObject(user);
String value = exp.getValue(ctx, String.class);
System.out.println("value = " + value);
表達(dá)式就一個(gè) username 字符串,將來執(zhí)行的時(shí)候,會(huì)自動(dòng)從 user 中找到 username 的值并返回。
當(dāng)然表達(dá)式也可以是方法,例如我在 User 類中添加如下兩個(gè)方法:
public String sayHello(Integer age) {
return "hello " + username + ";age=" + age;
}
public String sayHello() {
return "hello " + username;
}
我們就可以通過表達(dá)式調(diào)用這兩個(gè)方法,如下:
調(diào)用有參的 sayHello:
String expression = "sayHello(99)";
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(expression);
StandardEvaluationContext ctx = new StandardEvaluationContext();
User user = new User();
user.setAddress("廣州");
user.setUsername("javaboy");
user.setId(99);
ctx.setRootObject(user);
String value = exp.getValue(ctx, String.class);
System.out.println("value = " + value);
就直接寫方法名然后執(zhí)行就行了。
調(diào)用無參的 sayHello:
String expression = "sayHello";
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(expression);
StandardEvaluationContext ctx = new StandardEvaluationContext();
User user = new User();
user.setAddress("廣州");
user.setUsername("javaboy");
user.setId(99);
ctx.setRootObject(user);
String value = exp.getValue(ctx, String.class);
System.out.println("value = " + value);
這些就都好懂了。
甚至,我們的表達(dá)式也可以涉及到 Spring 中的一個(gè) Bean,例如我們向 Spring 中注冊(cè)如下 Bean:
@Service("us")
public class UserService {
public String sayHello(String name) {
return "hello " + name;
}
}
然后通過 SpEL 表達(dá)式來調(diào)用這個(gè)名為 us 的 bean 中的 sayHello 方法,如下:
@Autowired
BeanFactory beanFactory;
@Test
void contextLoads() {
String expression = "@us.sayHello('javaboy')";
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(expression);
StandardEvaluationContext ctx = new StandardEvaluationContext();
ctx.setBeanResolver(new BeanFactoryResolver(beanFactory));
String value = exp.getValue(ctx, String.class);
System.out.println("value = " + value);
}
給配置的上下文環(huán)境設(shè)置一個(gè) bean 解析器,這個(gè) bean 解析器會(huì)自動(dòng)跟進(jìn)名字從 Spring 容器中找打響應(yīng)的 bean 并執(zhí)行對(duì)應(yīng)的方法。
當(dāng)然,關(guān)于 SpEL 的玩法還有很多,我就不一一列舉了。這里主要是想讓小伙伴們知道,有這么個(gè)技術(shù),方便大家理解 @PreAuthorize 注解的原理。
總結(jié)一下:
1.在使用 SpEL 的時(shí)候,如果表達(dá)式直接寫的就是方法名,那是因?yàn)樵跇?gòu)建 SpEL 上下文的時(shí)候,已經(jīng)設(shè)置了 RootObject 了,我們所調(diào)用的方法,實(shí)際上就是 RootObject 對(duì)象中的方法。
2.在使用 SpEL 對(duì)象的時(shí)候,如果像調(diào)用非 RootObject 對(duì)象中的方法,那么表達(dá)式需要加上 @對(duì)象名 作為前綴,例如前面案例的 @us。
2. 自定義權(quán)限該如何寫
那么自定義權(quán)限到底該如何寫呢?首先我們來看下在 Spring Security 中,不涉及到通配符的權(quán)限該怎么處理。
松哥舉一個(gè)簡(jiǎn)單的例子,我們創(chuàng)建一個(gè) Spring Boot 工程,引入 Web 和 Security 依賴,為了方便,這里的用戶我直接創(chuàng)建在內(nèi)存中,配置如下:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Bean
UserDetailsService userDetailsService() {
InMemoryUserDetailsManager m = new InMemoryUserDetailsManager();
m.createUser(User.withUsername("javaboy").password("{noop}123").authorities("system:user:add","system:user:delete").build());
return m;
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.permitAll();
return http.build();
}
}
都是常規(guī)配置,沒啥好說的。注意前面的注解,開啟基于注解的權(quán)限控制。
這里我多啰嗦一句,大家看創(chuàng)建用戶的時(shí)候,調(diào)用的是 authorities 方法去設(shè)置權(quán)限的,這個(gè)跟 roles 方法其實(shí)沒啥大的區(qū)別,調(diào)用 roles 方法會(huì)自動(dòng)為你設(shè)置的字符串添加一個(gè) ROLE_ 前綴,其他的其實(shí)都一樣。在 Spring Security 中,role 和 permission 僅僅只是人為劃分出來的東西,底層的實(shí)現(xiàn)包括判斷邏輯基本上都是沒有區(qū)別的。
接下來我們定義四個(gè)測(cè)試接口,如下:
@RestController
public class UserController {
@GetMapping("/add")
@PreAuthorize("hasPermission('/add','system:user:add')")
public String addUser() {
return "add";
}
@GetMapping("/delete")
@PreAuthorize("hasPermission('/delete','system:user:delete')")
public String deleteUser() {
return "delete";
}
@GetMapping("/update")
@PreAuthorize("hasPermission('/update','system:user:update')")
public String updateUser() {
return "update";
}
@GetMapping("/select")
@PreAuthorize("hasPermission('/select','system:user:select')")
public String selectUser() {
return "select";
}
}
接口訪問都需要不同的權(quán)限。
此時(shí)如果大家啟動(dòng)項(xiàng)目去此時(shí),系統(tǒng)會(huì)提示你四個(gè)接口統(tǒng)統(tǒng)都不具備權(quán)限,這是啥原因呢?我們來繼續(xù)分析。
小伙伴們看這里,調(diào)用的時(shí)候 @PreAuthorize 注解中執(zhí)行寫方法名,不用寫對(duì)象名,說明調(diào)用的方法是 RootObject 中的方法,這里的 RootObject 實(shí)際上就是 SecurityExpressionRoot,我們來看看這個(gè)對(duì)象中的 hasPermission 方法:
@Override
public boolean hasPermission(Object target, Object permission) {
return this.permissionEvaluator.hasPermission(this.authentication, target, permission);
}
@Override
public boolean hasPermission(Object targetId, String targetType, Object permission) {
return this.permissionEvaluator.hasPermission(this.authentication, (Serializable) targetId, targetType,
permission);
}
最終的調(diào)用又指向了 permissionEvaluator 對(duì)象。
在 Spring Security 中,permissionEvaluator 有一個(gè)統(tǒng)一的接口就是 PermissionEvaluator,但是這個(gè)接口只有一個(gè)實(shí)現(xiàn)類,就是 DenyAllPermissionEvaluator,看名字就知道,這是拒絕所有。
public class DenyAllPermissionEvaluator implements PermissionEvaluator {
private final Log logger = LogFactory.getLog(getClass());
/**
* @return false always
*/
@Override
public boolean hasPermission(Authentication authentication, Object target, Object permission) {
return false;
}
/**
* @return false always
*/
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType,
Object permission) {
return false;
}
}
這兩個(gè)方法里啥都沒干,直接返回了 false,這下就破案了!
所以,在 Spring Security 中,如果想判斷權(quán)限,需要自己提供一個(gè) PermissionEvaluator 的實(shí)例,我們來看下:
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(permission)) {
return true;
}
}
return false;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
return false;
}
}
我這里的判斷邏輯比較簡(jiǎn)單,所以只需要實(shí)現(xiàn)第一個(gè)方法就行了,這個(gè)方法三個(gè)參數(shù),第一個(gè)參數(shù)就是當(dāng)前登錄成功的用戶對(duì)象,后面兩個(gè)參數(shù)則是我們?cè)?@PreAuthorize("hasPermission('/select','system:user:select')") 注解中的兩個(gè)參數(shù),現(xiàn)在該有的東西都有了,我們只需要判斷需要的權(quán)限當(dāng)前用戶是否有就行了。
這個(gè)自定義的權(quán)限評(píng)估器寫好之后,注冊(cè)到 Spring 容器就行了,其他什么事情都不用做。
接下來我們就可以對(duì)剛才的四個(gè)接口進(jìn)行測(cè)試了,測(cè)試過程我就不演示了,小伙伴們自行用 postman 測(cè)試就行了。
3. 權(quán)限通配符
看明白了上面的邏輯,現(xiàn)在不用我說,大家也知道權(quán)限通配符在 Spring Security 中是不支持的(無論你在 @PreAuthorize 注解中寫的 SpEL 是哪個(gè),調(diào)用的是哪個(gè)方法,都是不支持權(quán)限通配符的)。
例如我現(xiàn)在這樣描述我的用戶權(quán)限:
@Bean
UserDetailsService userDetailsService() {
InMemoryUserDetailsManager m = new InMemoryUserDetailsManager();
m.createUser(User.withUsername("javaboy").password("{noop}123").authorities("system:user:*").build());
return m;
}
我想用 system:user:* 字符串表示 javaboy 具有針對(duì)用戶的所有權(quán)限。
直接這樣寫肯定是不行的,最終字符串比較一定是不會(huì)通過的。
那么怎么辦呢?用正則似乎也不太行,因?yàn)?* 在正則中不代表所有字符,如果拆解字符串去比較,功能雖然也行得通,但是比較麻煩。
想來想去,想到一個(gè)辦法,不知道小伙伴們是否還記得我們之前在 vhr 中用過的 AntPathMatcher,用這個(gè)不就行了!
修改后的 CustomPermissionEvaluator 如下:
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (antPathMatcher.match(authority.getAuthority(), (String) permission)) {
return true;
}
}
return false;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
return false;
}
}
修改之后,現(xiàn)在只要用戶具備 system:user:* 權(quán)限,就四個(gè)接口都能訪問了。
4. TienChin 項(xiàng)目怎么做的
TienChin 項(xiàng)目用的是 RuoYi-Vue 腳手架,我們來看下這個(gè)腳手架的實(shí)現(xiàn)方式:
@PreAuthorize("@ss.hasPermi('tienchin:channel:query')")
@GetMapping("/list")
public TableDataInfo getChannelList() {
startPage();
List<Channel> list = channelService.list();
return getDataTable(list);
}
看了前面的講解,現(xiàn)在 @ss.hasPermi('tienchin:channel:query') 應(yīng)該很好懂了:
ss 是一個(gè)注冊(cè)在 Spring 容器中的 bean,對(duì)應(yīng)的類位于 org.javaboy.tienchin.framework.web.service.PermissionService 中。
很明顯,hasPermi 就是這個(gè)類中的方法。
這個(gè) hasPermi 方法的邏輯其實(shí)很簡(jiǎn)單:
public boolean hasPermi(String permission) {
if (StringUtils.isEmpty(permission)) {
return false;
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) {
return false;
}
return hasPermissions(loginUser.getPermissions(), permission);
}
private boolean hasPermissions(Set<String> permissions, String permission) {
return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
}
這個(gè)判斷邏輯很簡(jiǎn)單,就是獲取到當(dāng)前登錄的用戶,判斷當(dāng)前登錄用戶的權(quán)限集合中是否具備當(dāng)前請(qǐng)求所需要的權(quán)限。具體的判斷邏輯沒啥好說的,就是看集合中是否存在某個(gè)字符串,從判斷的邏輯中我們也可以看出來,這個(gè)權(quán)限也是不支持通配符的。
到此這篇關(guān)于詳解Spring Security如何在權(quán)限中使用通配符的文章就介紹到這了,更多相關(guān)Spring Security使用通配符內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
利用Spring插件實(shí)現(xiàn)策略模式的案例詳解
Spring插件提供了一種更實(shí)用的插件開發(fā)方法,它提供了插件實(shí)現(xiàn)擴(kuò)展核心系統(tǒng)功能的核心靈活性,但當(dāng)然不提供核心OSGi功能,如動(dòng)態(tài)類加載或運(yùn)行時(shí)安裝和部署插件,本文就來聊下如何使用spring插件來實(shí)現(xiàn)策略模式,需要的朋友可以參考下2023-05-05
java前后端使用ajax數(shù)據(jù)交互問題(簡(jiǎn)單demo)
這篇文章主要介紹了java前后端使用ajax數(shù)據(jù)交互問題(簡(jiǎn)單demo),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。2023-06-06
Springboot文件上傳功能簡(jiǎn)單測(cè)試
這篇文章主要介紹了Springboot文件上傳功能簡(jiǎn)單測(cè)試,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05
Springboot之自定義全局異常處理的實(shí)現(xiàn)
這篇文章主要介紹了Springboot之自定義全局異常處理的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08
基于SpringBoot bootstrap.yml配置未生效的解決
這篇文章主要介紹了基于SpringBoot bootstrap.yml配置未生效的解決方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-10-10
Java?使用geotools讀取tiff數(shù)據(jù)的示例代碼
這篇文章主要介紹了Java?通過geotools讀取tiff,一般對(duì)于tiff數(shù)據(jù)的讀取,都會(huì)借助于gdal,本文結(jié)合示例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-04-04
SpringBoot使用AOP實(shí)現(xiàn)統(tǒng)計(jì)全局接口訪問次數(shù)詳解
這篇文章主要介紹了SpringBoot通過AOP實(shí)現(xiàn)對(duì)全局接口訪問次數(shù)的統(tǒng)計(jì),文章從相關(guān)問題展開全文內(nèi)容詳情,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-06-06
玩轉(zhuǎn)spring boot 結(jié)合jQuery和AngularJs(3)
玩轉(zhuǎn)spring boot,這篇文章主要介紹了結(jié)合jQuery和AngularJs,玩轉(zhuǎn)spring boot,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01
Java單例模式實(shí)現(xiàn)靜態(tài)內(nèi)部類方法示例
這篇文章主要介紹了Java單例模式實(shí)現(xiàn)靜態(tài)內(nèi)部類方法示例,涉及構(gòu)造函數(shù)私有化等相關(guān)內(nèi)容,需要的朋友可以了解下。2017-09-09

