Java利用SpEL表達(dá)式實(shí)現(xiàn)權(quán)限校驗(yàn)
對于在Springboot中,利用自定義注解+切面來實(shí)現(xiàn)接口權(quán)限的控制這個(gè)大家應(yīng)該都很熟悉,也有大量的博客來介紹整個(gè)的實(shí)現(xiàn)過程,整體來說思路如下:
- 自定義一個(gè)權(quán)限校驗(yàn)的注解,包含參數(shù)value
- 配置在對應(yīng)的接口上
- 定義一個(gè)切面類,指定切點(diǎn)
- 在切入的方法體里寫上權(quán)限判斷的邏輯
乍一看,沒毛病,學(xué)到了,學(xué)到了~,收藏起來。但是呢,等到實(shí)際用到的時(shí)候就傻眼了,為什么呢?在實(shí)際的開發(fā)中,你會發(fā)現(xiàn),對于權(quán)限校驗(yàn)的需求場景是很多的,比如:
- 只要配置了任何角色,就可以訪問
- 有某個(gè)權(quán)限就可以訪問
- 放行所有請求
- 只有超級管理員角色才可以訪問
- 只有登錄后才可以訪問
- 在指定時(shí)間段內(nèi)可以訪問
- 有某個(gè)角色的情況下才可以訪問
- 同時(shí)具有指定的多個(gè)角色情況下才可以訪問
- 等
傻眼了不,按照上面的實(shí)現(xiàn)邏輯的話怎么搞?加注解?寫各種判斷?這時(shí)候,其實(shí)我們就可以通過SpEL表達(dá)式來幫我們處理這個(gè)問題。
Spring Security實(shí)現(xiàn)
Spring Security已經(jīng)幫我們實(shí)現(xiàn)了如何通過注解,直接上例子:
@PreAuthorize("@pms.hasPermission('1-6')") @ApiOperation("查 詢") @GetMapping public PageResponse<RoleCO> pageRole(RolePageQry qry){ return roleService.listPage(qry); }
通過@PreAuthorize("@pms.hasPermission('1-6')")
實(shí)現(xiàn)了訪問該接口,需要有1-6
這個(gè)權(quán)限。我們需要實(shí)現(xiàn)pms
public interface Pms { /** * 判斷接口是否有任意xxx,xxx權(quán)限 * * @param permission 權(quán)限 * @return {boolean} */ default boolean hasPermission(String permission) { if (StrUtil.isBlank(permission)) { return false; } return listPermissions().contains(permission); } // default boolean hasAnyPermission(String... permissions) { if (CollectionUtil.isEmpty(Arrays.asList(permissions))) { return false; } return listPermissions().stream().filter(StringUtils::hasText) .anyMatch(x -> PatternMatchUtils.simpleMatch(permissions, x)); } List<String> listPermissions(); }
@Data public class Pms implements com.jjb.saas.framework.api.system.Pms { /** * 菜單下有哪些按鈕權(quán)限 * * @param * @return {boolean} */ @Override public List<String> listPermissions() { MultiResponse multiResponse = FacadeServiceFactory.getInstance().genericInvoke("com.jjb.saas.system.client.permsgroup.facade.PermsGroupFacade", "listPermssionsByAppKey", MultiResponse.class); if (multiResponse.isSuccess()) { List datas = multiResponse.getData(); if (ObjectUtil.isNotEmpty(datas)) { return datas; } return Collections.emptyList(); } return Collections.emptyList(); } }
注入到spring中
@ComponentScan @EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING) @AllArgsConstructor public class SecurityConfiguration { @Bean("pms") @ConditionalOnMissingBean public Pms pms() { Pms pms = new Pms(); return pms; } }
接下來,我們就來看看如何自己造輪子
SpEL表達(dá)式
提到SpEL,那么到底SpEL是啥呢?
SpEL的全稱為Spring Expression Language,即Spring表達(dá)式語言。是Spring3.0提供的。他最強(qiáng)大的功能是可以通過運(yùn)行期間執(zhí)行的表達(dá)式將值裝配到我們的屬性或構(gòu)造函數(shù)之中。
如果有小伙伴之前沒有接觸過,不太理解這句話的含義,那么不要緊,繼續(xù)往下看,通過后續(xù)的實(shí)踐你就能明白他的作用了。
造輪子
自定義注解
當(dāng)然,萬變不離其宗,自定義注解我們還是需要滴。這里呢,我們僅需要定義一個(gè)value屬性用于接收表達(dá)式即可。
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface PreAuth { /** * * * permissionAll()-----只要配置了角色就可以訪問 * hasPermission("MENU.QUERY")-----有MENU.QUERY操作權(quán)限的角色可以訪問 * permitAll()-----放行所有請求 * denyAll()-----只有超級管理員角色才可訪問 * hasAuth()-----只有登錄后才可訪問 * hasTimeAuth(1,,10)-----只有在1-10點(diǎn)間訪問 * hasRole(‘管理員')-----具有管理員角色的人才能訪問 * hasAllRole(‘管理員','總工程師')-----同時(shí)具有管理員、總工程師角色的人才能訪問 * * Spring el * 文檔地址:https://docs.spring.io/spring/docs/5.1.6.RELEASE/spring-framework-reference/core.html#expressions */ String value(); }
定義切面
注解定義好了,我們就需要定義切面了。這里要考慮一個(gè)點(diǎn)。我們希望的是如果方法上有注解,則對方法進(jìn)行限制,若方法上無注解,單是類上有注解,那么類上的權(quán)限注解對該類下所有的接口生效。因此,我們切點(diǎn)的話要用@within注解。代碼如下:
@Around( "@annotation(PreAuth注解路徑) || " + "@within(PreAuth注解路徑)" ) public Object preAuth(ProceedingJoinPoint point) throws Throwable { if (handleAuth(point)) { return point.proceed(); } throw new SecureException(ResultCode.REQ_REJECT); } private boolean handleAuth(ProceedingJoinPoint point) { //TODO 邏輯判斷,返回true or false }
權(quán)限校驗(yàn)
關(guān)鍵點(diǎn)來了。這里我們要引入SpEL。
首先,引入SpEL:
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
然后,從注解上獲取我們需要的表達(dá)式:
MethodSignature ms = point.getSignature() instanceof MethodSignature? (MethodSignature) point.getSignature():null; Method method = ms.getMethod(); // 讀取權(quán)限注解,優(yōu)先方法上,沒有則讀取類 PreAuth preAuth = ClassUtil.getAnnotation(method, PreAuth.class); // 判斷表達(dá)式 String condition = preAuth.value(); if (StringUtil.isNotBlank(condition)) { //TODU 表達(dá)式解析 }
表達(dá)式解析
private boolean handleAuth(ProceedingJoinPoint point) { MethodSignature ms = point.getSignature() instanceof MethodSignature? (MethodSignature) point.getSignature():null; Method method = ms.getMethod(); // 讀取權(quán)限注解,優(yōu)先方法上,沒有則讀取類 PreAuth preAuth = ClassUtil.getAnnotation(method, PreAuth.class); // 判斷表達(dá)式 String condition = preAuth.value(); if (StringUtil.isNotBlank(condition)) { Expression expression = EXPRESSION_PARSER.parseExpression(condition); // 方法參數(shù)值 Object[] args = point.getArgs(); StandardEvaluationContext context = getEvaluationContext(method, args); //獲取解析計(jì)算的結(jié)果 return expression.getValue(context, Boolean.class); } return false; } /** * 獲取方法上的參數(shù) * * @param method 方法 * @param args 變量 * @return {SimpleEvaluationContext} */ private StandardEvaluationContext getEvaluationContext(Method method, Object[] args) { // 初始化Sp el表達(dá)式上下文,并設(shè)置 AuthFun StandardEvaluationContext context = new StandardEvaluationContext(new AuthFun()); // 設(shè)置表達(dá)式支持spring bean context.setBeanResolver(new BeanFactoryResolver(applicationContext)); for (int i = 0; i < args.length; i++) { // 讀取方法參數(shù) MethodParameter methodParam = ClassUtil.getMethodParameter(method, i); // 設(shè)置方法 參數(shù)名和值 為spel變量 context.setVariable(methodParam.getParameterName(), args[i]); } return context; }
自定義解析方法
看完上面的解析處理是不是很蒙蔽,只看到了獲取表達(dá)式,獲取參數(shù),設(shè)置參數(shù),然后expression.getValue
就完事了。有的同學(xué)會問,你權(quán)限校驗(yàn)的邏輯呢?
別急,關(guān)鍵點(diǎn)在這:StandardEvaluationContext context = new StandardEvaluationContext(new AuthFun());
在上文代碼中找到了吧。這個(gè)AuthFun就是我們進(jìn)行權(quán)限校驗(yàn)的對象。
所以呢,我們還得在定義一下這個(gè)對象。進(jìn)行具體的權(quán)限校驗(yàn)邏輯處理,這里定的每一個(gè)方法都可以作為表達(dá)式在權(quán)限注解中使用。代碼如下:
public class AuthFun { /** * 判斷角色是否具有接口權(quán)限 * * @return {boolean} */ public boolean permissionAll() { //TODO } /** * 判斷角色是否具有接口權(quán)限 * * @param permission 權(quán)限編號,對應(yīng)菜單的MENU_CODE * @return {boolean} */ public boolean hasPermission(String permission) { //TODO } /** * 放行所有請求 * * @return {boolean} */ public boolean permitAll() { return true; } /** * 只有超管角色才可訪問 * * @return {boolean} */ public boolean denyAll() { return hasRole(RoleConstant.ADMIN); } /** * 是否已授權(quán) * * @return {boolean} */ public boolean hasAuth() { if(Func.isEmpty(AuthUtil.getUser())){ // TODO 返回異常提醒 }else{ return true; } } /** * 是否有時(shí)間授權(quán) * * @param start 開始時(shí)間 * @param end 結(jié)束時(shí)間 * @return {boolean} */ public boolean hasTimeAuth(Integer start, Integer end) { Integer hour = DateUtil.hour(); return hour >= start && hour <= end; } /** * 判斷是否有該角色權(quán)限 * * @param role 單角色 * @return {boolean} */ public boolean hasRole(String role) { return hasAnyRole(role); } /** * 判斷是否具有所有角色權(quán)限 * * @param role 角色集合 * @return {boolean} */ public boolean hasAllRole(String... role) { for (String r : role) { if (!hasRole(r)) { return false; } } return true; } /** * 判斷是否有該角色權(quán)限 * * @param role 角色集合 * @return {boolean} */ public boolean hasAnyRole(String... role) { //獲取當(dāng)前登錄用戶 BladeUser user = AuthUtil.getUser(); if (user == null) { return false; } String userRole = user.getRoleName(); if (StringUtil.isBlank(userRole)) { return false; } String[] roles = Func.toStrArray(userRole); for (String r : role) { if (CollectionUtil.contains(roles, r)) { return true; } } return false; } }
實(shí)際使用
在使用的時(shí)候,我們只需要在類上或者接口上,加上@PreAuth的直接,value值寫的時(shí)候要注意一下,value應(yīng)該是我們在AuthFun類中定義的方法和參數(shù),如我們定義了解析方法hasAllRole(String... role)
,那么在注解中,我們就可以這樣寫@PreAuth("hasAllRole('角色1','角色2')")
,需要注意的是,參數(shù)要用單引號包括。
@PreAuth("hasPermission('LM_QUERY,LM_QUERY_ALL')") public T 接口名稱....
原理
根據(jù)上面的實(shí)際使用,可以看到。SpEL表達(dá)式解析將我們注解中的"hasAllRole('角色1','角色2')
"這樣的字符串,給動(dòng)態(tài)解析為了hasAllRole(參數(shù)1,參數(shù)1)
,并調(diào)用我們注冊類中同名的方法。
總結(jié)
通過SpEL的使用,讓我們的權(quán)限配置校驗(yàn)更加靈活。當(dāng)出現(xiàn)新的場景時(shí),我們僅需要在自定的表達(dá)式解析類中增加對應(yīng)場景的解析方法即可。相對于之前的實(shí)現(xiàn)方式,這不得不說是更好的一個(gè)選擇。
以上就是Java利用SpEL表達(dá)式實(shí)現(xiàn)權(quán)限校驗(yàn)的詳細(xì)內(nèi)容,更多關(guān)于Java權(quán)限校驗(yàn)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot配置Redis連接池的實(shí)現(xiàn)步驟
本文主要介紹了SpringBoot配置Redis連接池的實(shí)現(xiàn)步驟,詳細(xì)的講解了連接池的作用、配置方式、連接池參數(shù)說明,具有一定的參考價(jià)值,感興趣的可以了解一下2025-03-03Java操作redis設(shè)置第二天凌晨過期的解決方案
這篇文章主要介紹了Java操作redis設(shè)置第二天凌晨過期的解決方案,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01java發(fā)送http的get、post請求實(shí)現(xiàn)代碼
下面小編就為大家?guī)硪黄猨ava發(fā)送http的get、post請求實(shí)現(xiàn)代碼。小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-05-05Java項(xiàng)目啟動(dòng)成功、失敗信息實(shí)時(shí)反饋提醒問題(郵件或者短信)
這篇文章主要介紹了Java項(xiàng)目啟動(dòng)成功、失敗信息實(shí)時(shí)反饋提醒問題(郵件或者短信),具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-06-06Java String 和StringBuffer的詳解及區(qū)別
這篇文章主要介紹了Java String 和StringBuffer的詳解及區(qū)別的相關(guān)資料,需要的朋友可以參考下2017-05-05java結(jié)合keytool如何實(shí)現(xiàn)非對稱加密與解密詳解
這篇文章主要給大家介紹了關(guān)于java結(jié)合keytool如何實(shí)現(xiàn)非對稱加密與解密的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-08-08