詳解Spring Security中權(quán)限注解的使用
最近有個(gè)小伙伴在微信群里問(wèn) Spring Security 權(quán)限注解的問(wèn)題:
很多時(shí)候事情就是這么巧,松哥最近在做的 tienchin 也是基于注解來(lái)處理權(quán)限問(wèn)題的,所以既然大家有這個(gè)問(wèn)題,咱們就一塊來(lái)聊聊這個(gè)話題。
當(dāng)然一些基礎(chǔ)的知識(shí)我就不講了,對(duì)于 Spring Security 基本用法尚不熟悉的小伙伴,可在公眾號(hào)后臺(tái)回復(fù) ss,有原創(chuàng)的系列教程。
1. 具體用法
先來(lái)看看 Spring Security 權(quán)限注解的具體用法,如下:
@PreAuthorize("@ss.hasPermi('tienchin:channel:query')") @GetMapping("/list") public TableDataInfo getChannelList() { startPage(); List<Channel> list = channelService.list(); return getDataTable(list); }
類似于上面這樣,意思就是說(shuō),當(dāng)前用戶需要具備 tienchin:channel:query 權(quán)限,才能執(zhí)行當(dāng)前的接口方法。
那么要搞明白 @PreAuthorize 注解的原理,我覺(jué)得得從兩個(gè)方面入手:
- 首先明白 Spring 中提供的 SpEL。
- 其次搞明白 Spring Security 中對(duì)方法注解的處理規(guī)則。
我們一個(gè)一個(gè)來(lái)看。
2. SpEL
Spring Expression Language(簡(jiǎn)稱 SpEL)是一個(gè)支持查詢和操作運(yùn)行時(shí)對(duì)象導(dǎo)航圖功能的強(qiáng)大的表達(dá)式語(yǔ)言。它的語(yǔ)法類似于傳統(tǒng) EL,但提供額外的功能,最出色的就是函數(shù)調(diào)用和簡(jiǎn)單字符串的模板函數(shù)。
SpEL 給 Spring 社區(qū)提供一種簡(jiǎn)單而高效的表達(dá)式語(yǔ)言,一種可貫穿整個(gè) Spring 產(chǎn)品組的語(yǔ)言。這種語(yǔ)言的特性基于 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í)過(guò) SpEL,那么上面 Spring Security 那個(gè)注解其實(shí)很好理解。
我先通過(guò)一個(gè)簡(jiǎn)單的例子來(lái)和大家捋一捋 SpEL。
為了省事,我就創(chuàng)建一個(gè) Spring Boot 工程來(lái)和大家演示,創(chuàng)建的時(shí)候不用加任何額外的依賴,就最最基礎(chǔ)的依賴即可。
代碼如下:
String expressionStr = "1 + 2"; ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression(expressionStr);
expressionStr 是我們自定義的一個(gè)表達(dá)式字符串,這個(gè)字符串通過(guò)一個(gè) ExpressionParser 對(duì)象將之解析為一個(gè) Expression,接下來(lái)就可以執(zhí)行這個(gè) exp 了。
執(zhí)行的時(shí)候有兩種方式,對(duì)于我們上面這種不帶任何額外變量的,我們可以直接執(zhí)行,直接執(zhí)行的方式如下:
Object value = exp.getValue(); System.out.println(value.toString());
這個(gè)打印結(jié)果為 3。
我記得之前有個(gè)小伙伴在群里問(wèn)想執(zhí)行一個(gè)字符串表達(dá)式,但是不知道怎么辦,js 中有 eval 函數(shù)很方便,我們 Java 中也有 SpEL,一樣也很方便。
不過(guò)很多時(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 屬性。將來(lái)創(chuàng)建一個(gè) user 對(duì)象,放到 StandardEvaluationContext 中,并基于此對(duì)象執(zhí)行表達(dá)式,就可以打印出來(lái)想要的結(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 字符串,將來(lái)執(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; }
我們就可以通過(guò)表達(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)用無(wú)參的 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; } }
然后通過(guò) SpEL 表達(dá)式來(lái)調(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 注解的原理。
3. @PreAuthorize
接下來(lái)我們就回到 Spring Security 中來(lái)看 @PreAuthorize 注解。
權(quán)限的實(shí)現(xiàn)方式千千萬(wàn),又有各種不同的權(quán)限模型,然而歸結(jié)到代碼上,無(wú)非兩種:
基于 URL 地址的權(quán)限處理
基于方法注解的權(quán)限處理
松哥之前的 vhr 使用的是前者。
@PreAuthorize 注解當(dāng)然對(duì)應(yīng)的是后者。這次做的 tienchin 項(xiàng)目就是后者,我們來(lái)看一個(gè)例子:
@PreAuthorize("@ss.hasPermi('tienchin:channel:query')") @GetMapping("/list") public TableDataInfo getChannelList() { startPage(); List<Channel> list = channelService.list(); return getDataTable(list); }
注解好說(shuō),里邊的 @ss.hasPermi('tienchin:channel:query') 是啥意思呢?
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)限。具體的判斷邏輯沒(méi)啥好說(shuō)的,就是看集合中是否存在某個(gè)字符串。
那么這個(gè)方法是在哪里調(diào)用的呢?
大家知道,Spring Security 中處理權(quán)限的過(guò)濾器是 FilterSecurityInterceptor,所有的權(quán)限處理最終都會(huì)來(lái)到這個(gè)過(guò)濾器中。在這個(gè)過(guò)濾器中,將會(huì)用到各種投票器、表決器之類的工具,這里我就不細(xì)說(shuō)了,之前的 Spring Security 系列教程都有詳細(xì)介紹。
在投票器中,我們可以看到專門處理 @PreAuthorize 注解的類 PreInvocationAuthorizationAdviceVoter,我們來(lái)看下他里邊的核心方法:
@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; }
框架的源碼寫的就是好,你一看名字就知道他想干嘛了!這里就進(jìn)入到最后一句,調(diào)用了一個(gè) Advice 中到前置通知,來(lái)判斷權(quán)限是否滿足:
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; }
現(xiàn)在,當(dāng)你看到這個(gè) before 方法的時(shí)候,應(yīng)該會(huì)覺(jué)得比較熟悉了吧。
- 首先獲取到 preAttr 對(duì)象,這個(gè)對(duì)象里邊其實(shí)就保存著你 @PreAuthorize 注解中的內(nèi)容。
- 接下來(lái)跟進(jìn)當(dāng)前登錄用戶信息 authentication 創(chuàng)建一個(gè)上下文對(duì)象,此時(shí)創(chuàng)建出來(lái)的上下文對(duì)象中就包含了當(dāng)前用戶具備哪些權(quán)限。
- 獲取過(guò)濾器(我們這個(gè)項(xiàng)目中無(wú))。
- 獲取到權(quán)限注解。
- 最后執(zhí)行表達(dá)式,去查看當(dāng)前用戶權(quán)限中是否包含請(qǐng)求所需要的權(quán)限。
就這樣,是不是很簡(jiǎn)單?
到此這篇關(guān)于詳解Spring Security中權(quán)限注解的使用的文章就介紹到這了,更多相關(guān)Spring Security權(quán)限注解內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringCloud網(wǎng)關(guān)(Zuul)如何給多個(gè)微服務(wù)之間傳遞共享參數(shù)
這篇文章主要介紹了SpringCloud網(wǎng)關(guān)(Zuul)如何給多個(gè)微服務(wù)之間傳遞共享參數(shù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03mybatis實(shí)現(xiàn)遍歷Map的key和value
這篇文章主要介紹了mybatis實(shí)現(xiàn)遍歷Map的key和value方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01Mybatis Plus 代碼生成器的實(shí)現(xiàn)
這篇文章主要介紹了Mybatis Plus 代碼生成器的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03詳解如何通過(guò)Java實(shí)現(xiàn)類似Nginx代理
最近遇到一個(gè)問(wèn)題,在內(nèi)網(wǎng)環(huán)境中部署的項(xiàng)目需要調(diào)用外網(wǎng)完成一些應(yīng)用,一般情況我們可以通過(guò)增加一臺(tái)機(jī)器,部署到可以訪問(wèn)外網(wǎng)的服務(wù)器上,然后內(nèi)網(wǎng)直接連接該機(jī)器通過(guò)Nginx進(jìn)行代理即可,所以本文介紹了如何通過(guò)Java實(shí)現(xiàn)類似Nginx代理,需要的朋友可以參考下2024-08-08JDK17在Windows安裝及環(huán)境變量配置超詳細(xì)的教程
這篇文章主要介紹了JDK17在Windows安裝及環(huán)境變量配置超詳細(xì)的教程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-11-11