詳解Spring Security中權限註解的使用
最近有個小夥伴在微信群裡問 Spring Security 權限註解的問題:
很多時候事情就是這麼巧,松哥最近在做的 tienchin 也是基於註解來處理權限問題的,所以既然大傢有這個問題,咱們就一塊來聊聊這個話題。
當然一些基礎的知識我就不講瞭,對於 Spring Security 基本用法尚不熟悉的小夥伴,可在公眾號後臺回復 ss,有原創的系列教程。
1. 具體用法
先來看看 Spring Security 權限註解的具體用法,如下:
@PreAuthorize("@ss.hasPermi('tienchin:channel:query')") @GetMapping("/list") public TableDataInfo getChannelList() { startPage(); List<Channel> list = channelService.list(); return getDataTable(list); }
類似於上面這樣,意思就是說,當前用戶需要具備 tienchin:channel:query 權限,才能執行當前的接口方法。
那麼要搞明白 @PreAuthorize 註解的原理,我覺得得從兩個方面入手:
- 首先明白 Spring 中提供的 SpEL。
- 其次搞明白 Spring Security 中對方法註解的處理規則。
我們一個一個來看。
2. SpEL
Spring Expression Language(簡稱 SpEL)是一個支持查詢和操作運行時對象導航圖功能的強大的表達式語言。它的語法類似於傳統 EL,但提供額外的功能,最出色的就是函數調用和簡單字符串的模板函數。
SpEL 給 Spring 社區提供一種簡單而高效的表達式語言,一種可貫穿整個 Spring 產品組的語言。這種語言的特性基於 Spring 產品的需求而設計,這是它出現的一大特色。
在我們離不開 Spring 框架的同時,其實我們也已經離不開 SpEL 瞭,因為它太好用、太強大瞭,SpEL 在整個 Spring 傢族中也處於一個非常重要的位置。但是很多時候,我們對它的隻瞭解一個大概,其實如果你系統的學習過 SpEL,那麼上面 Spring Security 那個註解其實很好理解。
我先通過一個簡單的例子來和大傢捋一捋 SpEL。
為瞭省事,我就創建一個 Spring Boot 工程來和大傢演示,創建的時候不用加任何額外的依賴,就最最基礎的依賴即可。
代碼如下:
String expressionStr = "1 + 2"; ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression(expressionStr);
expressionStr 是我們自定義的一個表達式字符串,這個字符串通過一個 ExpressionParser 對象將之解析為一個 Expression,接下來就可以執行這個 exp 瞭。
執行的時候有兩種方式,對於我們上面這種不帶任何額外變量的,我們可以直接執行,直接執行的方式如下:
Object value = exp.getValue(); System.out.println(value.toString());
這個打印結果為 3。
我記得之前有個小夥伴在群裡問想執行一個字符串表達式,但是不知道怎麼辦,js 中有 eval 函數很方便,我們 Java 中也有 SpEL,一樣也很方便。
不過很多時候,我們要執行的表達式可能比較復雜,這時候上面這種調用方式就不太夠用瞭。
此時我們可以為要調用的表達式設置一個上下文環境,這個時候就會用到 EvaluationContext 或者它的子類,如下:
StandardEvaluationContext context = new StandardEvaluationContext(); System.out.println(exp.getValue(context));
當然上面這個表達式不需要設置上下文環境,我舉一個需要設置上下文環境的例子。
例如我現在有一個 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 屬性。將來創建一個 user 對象,放到 StandardEvaluationContext 中,並基於此對象執行表達式,就可以打印出來想要的結果。
如果我們將 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 字符串,將來執行的時候,會自動從 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);
就直接寫方法名然後執行就行瞭。
調用無參的 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); }
給配置的上下文環境設置一個 bean 解析器,這個 bean 解析器會自動跟進名字從 Spring 容器中找打響應的 bean 並執行對應的方法。
當然,關於 SpEL 的玩法還有很多,我就不一一列舉瞭。這裡主要是想讓小夥伴們知道,有這麼個技術,方便大傢理解 @PreAuthorize 註解的原理。
3. @PreAuthorize
接下來我們就回到 Spring Security 中來看 @PreAuthorize 註解。
權限的實現方式千千萬,又有各種不同的權限模型,然而歸結到代碼上,無非兩種:
基於 URL 地址的權限處理
基於方法註解的權限處理
松哥之前的 vhr 使用的是前者。
@PreAuthorize 註解當然對應的是後者。這次做的 tienchin 項目就是後者,我們來看一個例子:
@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 中處理權限的過濾器是 FilterSecurityInterceptor,所有的權限處理最終都會來到這個過濾器中。在這個過濾器中,將會用到各種投票器、表決器之類的工具,這裡我就不細說瞭,之前的 Spring Security 系列教程都有詳細介紹。
在投票器中,我們可以看到專門處理 @PreAuthorize 註解的類 PreInvocationAuthorizationAdviceVoter,我們來看下他裡邊的核心方法:
@Override public int vote(Authentication authentication, MethodInvocation method, Collection<ConfigAttribute> attributes) { PreInvocationAttribute preAttr = findPreInvocationAttribute(attributes); if (preAttr == null) { return ACCESS_ABSTAIN; } return this.preAdvice.before(authentication, method, preAttr) ? ACCESS_GRANTED : ACCESS_DENIED; }
框架的源碼寫的就是好,你一看名字就知道他想幹嘛瞭!這裡就進入到最後一句,調用瞭一個 Advice 中到前置通知,來判斷權限是否滿足:
public boolean before(Authentication authentication, MethodInvocation mi, PreInvocationAttribute attr) { PreInvocationExpressionAttribute preAttr = (PreInvocationExpressionAttribute) attr; EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, mi); Expression preFilter = preAttr.getFilterExpression(); Expression preAuthorize = preAttr.getAuthorizeExpression(); if (preFilter != null) { Object filterTarget = findFilterTarget(preAttr.getFilterTarget(), ctx, mi); this.expressionHandler.filter(filterTarget, preFilter, ctx); } return (preAuthorize != null) ? ExpressionUtils.evaluateAsBoolean(preAuthorize, ctx) : true; }
現在,當你看到這個 before 方法的時候,應該會覺得比較熟悉瞭吧。
- 首先獲取到 preAttr 對象,這個對象裡邊其實就保存著你 @PreAuthorize 註解中的內容。
- 接下來跟進當前登錄用戶信息 authentication 創建一個上下文對象,此時創建出來的上下文對象中就包含瞭當前用戶具備哪些權限。
- 獲取過濾器(我們這個項目中無)。
- 獲取到權限註解。
- 最後執行表達式,去查看當前用戶權限中是否包含請求所需要的權限。
就這樣,是不是很簡單?
到此這篇關於詳解Spring Security中權限註解的使用的文章就介紹到這瞭,更多相關Spring Security權限註解內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 使用Spring Expression Language (SpEL)全面解析表達式
- springboot通過spel結合aop實現動態傳參的案例
- Spring Security密碼解析器PasswordEncoder自定義登錄邏輯
- Spring Security認證的完整流程記錄
- 在Spring-Boot中如何使用@Value註解註入集合類