欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解Spring Security如何在權限中使用通配符

 更新時間:2022年06月28日 14:17:39   作者:_江南一點雨  
小伙伴們知道,在Shiro中,默認是支持權限通配符的?,F在給用戶授權的時候,可以一個權限一個權限的配置,也可以直接用通配符。本文將介紹Spring Security如何在權限中使用通配符,需要的可以參考一下

前言

小伙伴們知道,在 Shiro 中,默認是支持權限通配符的,例如系統用戶有如下一些權限:

  • system:user:add
  • system:user:delete
  • system:user:select
  • system:user:update

現在給用戶授權的時候,我們可以像上面這樣,一個權限一個權限的配置,也可以直接用通配符:

system:user:*

這個通配符就表示擁有針對用戶的所有權限。

今天我們來聊聊 Spring Security 中對此如何處理,也順便來看看 TienChin 項目中,這塊該如何改進。

1. SpEL

要搞明白基于注解的權限管理,那么得首先理解 SpEL,不需要了解多深入,我這里就簡單介紹下。

Spring Expression Language(簡稱 SpEL)是一個支持查詢和操作運行時對象導航圖功能的強大的表達式語言。它的語法類似于傳統 EL,但提供額外的功能,最出色的就是函數調用和簡單字符串的模板函數。

SpEL 給 Spring 社區(qū)提供一種簡單而高效的表達式語言,一種可貫穿整個 Spring 產品組的語言。這種語言的特性基于 Spring 產品的需求而設計,這是它出現的一大特色。

在我們離不開 Spring 框架的同時,其實我們也已經離不開 SpEL 了,因為它太好用、太強大了,SpEL 在整個 Spring 家族中也處于一個非常重要的位置。但是很多時候,我們對它的只了解一個大概,其實如果你系統的學習過 SpEL,那么上面 Spring Security 那個注解其實很好理解。

我先通過一個簡單的例子來和大家捋一捋 SpEL。

為了省事,我就創(chuàng)建一個 Spring Boot 工程來和大家演示,創(chuàng)建的時候不用加任何額外的依賴,就最最基礎的依賴即可。

代碼如下:

String expressionStr = "1 + 2";
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(expressionStr);

expressionStr 是我們自定義的一個表達式字符串,這個字符串通過一個 ExpressionParser 對象將之解析為一個 Expression,接下來就可以執(zhí)行這個 exp 了。

執(zhí)行的時候有兩種方式,對于我們上面這種不帶任何額外變量的,我們可以直接執(zhí)行,直接執(zhí)行的方式如下:

Object value = exp.getValue();
System.out.println(value.toString());

這個打印結果為 3。

我記得之前有個小伙伴在群里問想執(zhí)行一個字符串表達式,但是不知道怎么辦,js 中有 eval 函數很方便,我們 Java 中也有 SpEL,一樣也很方便。

不過很多時候,我們要執(zhí)行的表達式可能比較復雜,這時候上面這種調用方式就不太夠用了。

此時我們可以為要調用的表達式設置一個上下文環(huán)境,這個時候就會用到 EvaluationContext 或者它的子類,如下:

StandardEvaluationContext context = new StandardEvaluationContext();
System.out.println(exp.getValue(context));

當然上面這個表達式不需要設置上下文環(huán)境,我舉一個需要設置上下文環(huán)境的例子。

例如我現在有一個 User 類,如下:

public class User {
    private Integer id;
    private String username;
    private String address;
    //省略 getter/setter
}

現在我的表達式是這樣:

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);

這個表達式就表示獲取 user 對象的 username 屬性。將來創(chuàng)建一個 user 對象,放到 StandardEvaluationContext 中,并基于此對象執(zhí)行表達式,就可以打印出來想要的結果。

如果我們將 user 對象設置為 rootObject,那么表達式中就不需要 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);

表達式就一個 username 字符串,將來執(zhí)行的時候,會自動從 user 中找到 username 的值并返回。

當然表達式也可以是方法,例如我在 User 類中添加如下兩個方法:

public String sayHello(Integer age) {
    return "hello " + username + ";age=" + age;
}
public String sayHello() {
    return "hello " + username;
}

我們就可以通過表達式調用這兩個方法,如下:

調用有參的 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í)行就行了。

調用無參的 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);

這些就都好懂了。

甚至,我們的表達式也可以涉及到 Spring 中的一個 Bean,例如我們向 Spring 中注冊如下 Bean:

@Service("us")
public class UserService {
    public String sayHello(String name) {
        return "hello " + name;
    }
}

然后通過 SpEL 表達式來調用這個名為 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)境設置一個 bean 解析器,這個 bean 解析器會自動跟進名字從 Spring 容器中找打響應的 bean 并執(zhí)行對應的方法。

當然,關于 SpEL 的玩法還有很多,我就不一一列舉了。這里主要是想讓小伙伴們知道,有這么個技術,方便大家理解 @PreAuthorize 注解的原理。

總結一下:

1.在使用 SpEL 的時候,如果表達式直接寫的就是方法名,那是因為在構建 SpEL 上下文的時候,已經設置了 RootObject 了,我們所調用的方法,實際上就是 RootObject 對象中的方法。

2.在使用 SpEL 對象的時候,如果像調用非 RootObject 對象中的方法,那么表達式需要加上 @對象名 作為前綴,例如前面案例的 @us。

2. 自定義權限該如何寫

那么自定義權限到底該如何寫呢?首先我們來看下在 Spring Security 中,不涉及到通配符的權限該怎么處理。

松哥舉一個簡單的例子,我們創(chuàng)建一個 Spring Boot 工程,引入 Web 和 Security 依賴,為了方便,這里的用戶我直接創(chuàng)建在內存中,配置如下:

@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ī)配置,沒啥好說的。注意前面的注解,開啟基于注解的權限控制。

這里我多啰嗦一句,大家看創(chuàng)建用戶的時候,調用的是 authorities 方法去設置權限的,這個跟 roles 方法其實沒啥大的區(qū)別,調用 roles 方法會自動為你設置的字符串添加一個 ROLE_ 前綴,其他的其實都一樣。在 Spring Security 中,role 和 permission 僅僅只是人為劃分出來的東西,底層的實現包括判斷邏輯基本上都是沒有區(qū)別的。

接下來我們定義四個測試接口,如下:

@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";
    }
}

接口訪問都需要不同的權限。

此時如果大家啟動項目去此時,系統會提示你四個接口統統都不具備權限,這是啥原因呢?我們來繼續(xù)分析。

小伙伴們看這里,調用的時候 @PreAuthorize 注解中執(zhí)行寫方法名,不用寫對象名,說明調用的方法是 RootObject 中的方法,這里的 RootObject 實際上就是 SecurityExpressionRoot,我們來看看這個對象中的 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);
}

最終的調用又指向了 permissionEvaluator 對象。

在 Spring Security 中,permissionEvaluator 有一個統一的接口就是 PermissionEvaluator,但是這個接口只有一個實現類,就是 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;
	}

}

這兩個方法里啥都沒干,直接返回了 false,這下就破案了!

所以,在 Spring Security 中,如果想判斷權限,需要自己提供一個 PermissionEvaluator 的實例,我們來看下:

@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;
    }
}

我這里的判斷邏輯比較簡單,所以只需要實現第一個方法就行了,這個方法三個參數,第一個參數就是當前登錄成功的用戶對象,后面兩個參數則是我們在 @PreAuthorize("hasPermission('/select','system:user:select')") 注解中的兩個參數,現在該有的東西都有了,我們只需要判斷需要的權限當前用戶是否有就行了。

這個自定義的權限評估器寫好之后,注冊到 Spring 容器就行了,其他什么事情都不用做。

接下來我們就可以對剛才的四個接口進行測試了,測試過程我就不演示了,小伙伴們自行用 postman 測試就行了。

3. 權限通配符

看明白了上面的邏輯,現在不用我說,大家也知道權限通配符在 Spring Security 中是不支持的(無論你在 @PreAuthorize 注解中寫的 SpEL 是哪個,調用的是哪個方法,都是不支持權限通配符的)。

例如我現在這樣描述我的用戶權限:

@Bean
UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager m = new InMemoryUserDetailsManager();
    m.createUser(User.withUsername("javaboy").password("{noop}123").authorities("system:user:*").build());
    return m;
}

我想用 system:user:* 字符串表示 javaboy 具有針對用戶的所有權限。

直接這樣寫肯定是不行的,最終字符串比較一定是不會通過的。

那么怎么辦呢?用正則似乎也不太行,因為 * 在正則中不代表所有字符,如果拆解字符串去比較,功能雖然也行得通,但是比較麻煩。

想來想去,想到一個辦法,不知道小伙伴們是否還記得我們之前在 vhr 中用過的 AntPathMatcher,用這個不就行了!

修改后的 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;
    }
}

修改之后,現在只要用戶具備 system:user:* 權限,就四個接口都能訪問了。

4. TienChin 項目怎么做的

TienChin 項目用的是 RuoYi-Vue 腳手架,我們來看下這個腳手架的實現方式:

@PreAuthorize("@ss.hasPermi('tienchin:channel:query')")
@GetMapping("/list")
public TableDataInfo getChannelList() {
    startPage();
    List<Channel> list = channelService.list();
    return getDataTable(list);
}

看了前面的講解,現在 @ss.hasPermi('tienchin:channel:query') 應該很好懂了:

ss 是一個注冊在 Spring 容器中的 bean,對應的類位于 org.javaboy.tienchin.framework.web.service.PermissionService 中。

很明顯,hasPermi 就是這個類中的方法。

這個 hasPermi 方法的邏輯其實很簡單:

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));
}

這個判斷邏輯很簡單,就是獲取到當前登錄的用戶,判斷當前登錄用戶的權限集合中是否具備當前請求所需要的權限。具體的判斷邏輯沒啥好說的,就是看集合中是否存在某個字符串,從判斷的邏輯中我們也可以看出來,這個權限也是不支持通配符的。

到此這篇關于詳解Spring Security如何在權限中使用通配符的文章就介紹到這了,更多相關Spring Security使用通配符內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • 詳解Spring Security中獲取當前登錄用戶的詳細信息的幾種方法

    詳解Spring Security中獲取當前登錄用戶的詳細信息的幾種方法

    本文主要介紹了詳解Spring Security中獲取當前登錄用戶的詳細信息的幾種方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2022-05-05
  • java如何發(fā)送get請求獲取數據(附代碼)

    java如何發(fā)送get請求獲取數據(附代碼)

    這篇文章主要給大家介紹了關于java如何發(fā)送get請求獲取數據的相關資料,Java中的GET請求方法是HTTP協議中的一種請求方式,用于向服務器請求獲取資源,需要的朋友可以參考下
    2023-10-10
  • 詳解Servlet 3.0/3.1 中的異步處理

    詳解Servlet 3.0/3.1 中的異步處理

    這篇文章主要介紹了詳解Servlet 3.0/3.1 中的異步處理,實例分析了servlet 3.0異步處理的技巧,非常具有實用價值,需要的朋友可以參考下
    2017-04-04
  • java多線程編程之使用thread類創(chuàng)建線程

    java多線程編程之使用thread類創(chuàng)建線程

    在Java中創(chuàng)建線程有兩種方法:使用Thread類和使用Runnable接口。在使用Runnable接口時需要建立一個Thread實例
    2014-01-01
  • Java實現求小于n的質數的3種方法

    Java實現求小于n的質數的3種方法

    這篇文章主要介紹了Java實現求小于n的質數的3種方法,本文給出了根據定義去求解、平方根、找規(guī)律三種解法,需要的朋友可以參考下
    2015-03-03
  • Java中的Graphics2D類基本使用教程

    Java中的Graphics2D類基本使用教程

    這篇文章主要介紹了Java中的Graphics2D類基本使用教程,Graphics2D類較之Graphics類中的功能更加專業(yè),需要的朋友可以參考下
    2015-10-10
  • Java基于Socket實現HTTP下載客戶端

    Java基于Socket實現HTTP下載客戶端

    這篇文章主要介紹了Java基于Socket實現HTTP下載客戶端的相關資料,感興趣的小伙伴們可以參考一下
    2016-01-01
  • java基本教程之多線程基本概念 java多線程教程

    java基本教程之多線程基本概念 java多線程教程

    多線程是Java中不可避免的一個重要主體。下面是對“JDK中新增JUC包”之前的Java多線程內容的講解,JUC包是由Java大師Doug Lea完成并在JDK1.5版本添加到Java中的
    2014-01-01
  • Mybatis 簡介與原理

    Mybatis 簡介與原理

    MyBatis 是支持定制化 SQL、存儲過程以及高級映射的優(yōu)秀的持久層框架。MyBatis 避免了幾乎所有的 JDBC 代碼和手動設置參數以及獲取結果集
    2017-05-05
  • 詳解Java 中的UnitTest 和 PowerMock

    詳解Java 中的UnitTest 和 PowerMock

    這篇文章主要介紹了Java中的 UnitTest 和 PowerMock,文中講解非常詳細,對大家學習有很大的幫助,感興趣的朋友可以了解下
    2020-06-06

最新評論