目錄
- 參考
- 一、概念
- SpEL表達式
- 二、開發
- 引入包
- 定義注解
- 定義切面
- 定義用戶上下文
- 三、測試
- 新建Service在方法上注解
- 新建Service在類上注解
- 運行
參考
SpringBoot:SpEL讓復雜權限控制變得很簡單
一、概念
對于在Springboot中,利用自定義注解+切面來實現接口權限的控制這個大家應該都很熟悉,也有大量的博客來介紹整個的實現過程,整體來說思路如下:
- 自定義一個權限校驗的注解,包含參數value
- 配置在對應的接口上
- 定義一個切面類,指定切點
- 在切入的方法體里寫上權限判斷的邏輯
SpEL表達式
本文前面提到SpEL,那么到底SpEL是啥呢? SpEL的全稱為Spring Expression Language,即Spring表達式語言。是Spring3.0提供的。他最強大的功能是可以通過運行期間執行的表達式將值裝配到我們的屬性或構造函數之中。如果有小伙伴之前沒有接觸過,不太理解這句話的含義,那么不要緊,繼續往下看,通過后續的實踐你就能明白他的作用了。
二、開發
引入包
<!--spring aop + aspectj--><dependency><groupId>org.springframework</groupId><artifactId>spring-aop</artifactId><version>5.0.8.RELEASE</version></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjrt</artifactId><version>1.8.9</version></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.8.9</version></dependency><!--spring aop + aspectj-->
定義注解
我們僅需要定義一個value屬性用于接收表達式即可。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PreAuth {/***** permissionAll()-----只要配置了角色就可以訪問* hasPermission("MENU.QUERY")-----有MENU.QUERY操作權限的角色可以訪問* hasAnyPermission("MENU.QUERY","MENU.ADD")-----有MENU.QUERY操作權限的角色可以訪問* permitAll()-----放行所有請求* denyAll()-----只有超級管理員角色才可訪問* hasAuth()-----只有登錄后才可訪問* hasTimeAuth(1,,10)-----只有在1-10點間訪問* hasRole(‘管理員’)-----具有管理員角色的人才能訪問* hasAnyRole(‘管理員’,'總工程師')-----同時具有管理員* hasAllRole(‘管理員’,'總工程師')-----同時具有管理員、總工程師角色的人才能訪問、總工程師角色的人才能訪問** Spring el* 文檔地址:<a href="https://docs.spring.io/spring/docs/5.1.6.RELEASE/spring-framework-reference/core.html#expressions">...</a>*/String value();}
定義切面
我們就需要定義切面了。這里要考慮一個點。我們希望的是如果方法上有注解,則對方法進行限制,若方法上無注解,單是類上有注解,那么類上的權限注解對該類下所有的接口生效。因此,我們切點的話要用@within
注解
// 方式一
@Pointcut(value = "execution(* com.edevp.spring.spel.auth..*.*(..))")
// 方式二 直接切入注解
@Pointcut("@annotation(com.edevp.spring.spel.auth.annotation.PreAuth) || @within(com.edevp.spring.spel.auth.annotation.PreAuth)")
/*** 必須的注解* @create 2023/5/24*/
@Component
@Aspect
@Slf4j
public class AuthAspect {@Resourceprivate AuthContext authContext;@PostConstructpublic void init(){log.info("鑒權切面初始化");}/*** Spel解析器 關鍵點來了。這里我們要引入SpEL。*/private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();// @Pointcut(value = "execution(* com.edevp.spring.spel.auth..*.*(..))")@Pointcut("@annotation(com.edevp.spring.spel.auth.annotation.PreAuth) || @within(com.edevp.spring.spel.auth.annotation.PreAuth)")private void beforePointcut(){//切面,方法里的內容不會執行}/*** 前置通知* @param joinPoint 切點*/@Before(value = "beforePointcut()")public void before(JoinPoint joinPoint){//@Before是在方法執行前無法終止原方法執行log.info("前置通知。。。"+joinPoint);if (handleAuth(joinPoint)) {return;}throw new NoAuthException("沒權限");}/*** 環繞通知* @param joinPoint 切點* @return Object*/@Around("beforePointcut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {//@Before是在方法執行前無法終止原方法執行log.info("環繞通知。。。"+joinPoint);return joinPoint.proceed();}/*** 判斷是否有權限* @param point 切點* @return boolean*/@SuppressWarnings("unchecked")private boolean handleAuth(JoinPoint point) {MethodSignature ms = point.getSignature() instanceof MethodSignature? (MethodSignature) point.getSignature():null;assert ms != null;Method method = ms.getMethod();// 讀取權限注解,優先方法上,沒有則讀取類PreAuth preAuth = method.getAnnotation(PreAuth.class);if(preAuth == null){preAuth = (PreAuth) ms.getDeclaringType().getDeclaredAnnotation(PreAuth.class);}// 判斷表達式String condition = preAuth.value();if (StringUtil.isNotBlank(condition)) {Expression expression = EXPRESSION_PARSER.parseExpression(condition);StandardEvaluationContext context = new StandardEvaluationContext(authContext);// 獲取解析計算的結果return Boolean.TRUE.equals(expression.getValue(context, Boolean.class));}return false;}
}
定義用戶上下文
有的同學會問,你權限校驗的邏輯呢?別急,關鍵點在這:StandardEvaluationContext context = new StandardEvaluationContext(authContext );在上文代碼中找到了吧。
這個AuthFun就是我們進行權限校驗的對象。所以呢,我們還得在定義一下這個對象。進行具體的權限校驗邏輯處理,這里定的每一個方法都可以作為表達式在權限注解中使用。代碼如下:
方法對應PreAuth中的方法字符串
@Component
public class AuthContext {private static final ThreadLocal<UserContext> USER_CONTEXT_THREAD_LOCAL = new NamedThreadLocal<>("user context");public static void setUserContext(UserContext user){USER_CONTEXT_THREAD_LOCAL.set(user);}public static UserContext getUserContext(){return USER_CONTEXT_THREAD_LOCAL.get();}public static void removeUserContext(){USER_CONTEXT_THREAD_LOCAL.remove();}/*** 判斷角色是否具有接口權限** @param permission 權限編號,對應菜單的MENU_CODE* @return {boolean}*/public boolean hasPermission(String permission) {//TODOreturn hasAnyPermission(permission);}/*** 判斷角色是否具有接口權限** @param permission 權限編號,對應菜單的MENU_CODE* @return {boolean}*/public boolean hasAllPermission(String... permission) {//TODOfor (String r : permission) {if (!hasPermission(r)) {return false;}}return true;}/*** 放行所有請求** @return {boolean}*/public boolean permitAll() {return true;}/*** 只有超管角色才可訪問** @return {boolean}*/public boolean denyAll() {return hasRole("admin");}/*** 是否有時間授權** @param start 開始時間* @param end 結束時間* @return {boolean}*/public boolean hasTimeAuth(Integer start, Integer end) {/*Integer hour = DateUtil.hour();return hour >= start && hour <= end;*/return true;}/*** 判斷是否有該角色權限** @param role 單角色* @return {boolean}*/public boolean hasRole(String role) {return hasAnyRole(role);}/*** 判斷是否具有所有角色權限** @param role 角色集合* @return {boolean}*/public boolean hasAllRole(String... role) {for (String r : role) {if (!hasRole(r)) {return false;}}return true;}/*** 判斷是否有該角色權限** @param roles 角色集合* @return {boolean}*/public boolean hasAnyRole(String... roles) {UserContext user = getUser();if(user!= null){return hasAnyStr(user.getRoles(),roles);}return false;}/*** 判斷是否有該角色權限** @param authorities 角色集合* @return {boolean}*/public boolean hasAnyPermission(String... authorities) {UserContext user = getUser();if(user!= null){return hasAnyStr(user.getAuthorities(),authorities);}return false;}public boolean hasAnyStr(String hasStrings,String... strings) {if(StringUtil.isNotEmpty(hasStrings)){String[] roleArr = hasStrings.split(SymbolConstant.COMMA);return Arrays.stream(strings).anyMatch(r-> Arrays.asList(roleArr).contains(r));}return false;}public UserContext getUser(){UserContext o = AuthContext.getUserContext();if(o != null){return o;}return null;}}
三、測試
在使用的時候,我們只需要在類上或者接口上,加上@PreAuth的直接,value值寫的時候要注意一下,value應該是我們在AuthContext 類中定義的方法和參數,如我們定義了解析方法hasAllRole(String… role),那么在注解中,我們就可以這樣寫@PreAuth(“hasAllRole(‘角色1’,‘角色2’)”),需要注意的是,參數要用單引號包括。
根據上面的實際使用,可以看到。SpEL表達式解析將我們注解中的"hasAllRole(‘角色1’,‘角色2’)"這樣的字符串,給動態解析為了hasAllRole(參數1,參數1),并調用我們注冊類中同名的方法。
新建Service在方法上注解
@Slf4j
@Component
public class AuthTestMethodService {@PreAuth("hasRole('admin')")public void testHasRole(){log.info("測試 hasRole('admin')");}@PreAuth("hasAnyRole('admin','test')")public void testHasAnyRole(){log.info("測試 testHasAnyRole('admin')");}@PreAuth("hasAllRole('admin','test')")public void testHasAllRole(){log.info("測試 testHasAllRole('admin')");}@PreAuth("hasPermission('sys:user:add')")public void testHasPermission(){log.info("測試 hasPermission('admin')");}
}
新建Service在類上注解
@Slf4j
@Component
@PreAuth("hasRole('admin')")
public class AuthTestClassService {public void testHasRole(){log.info("測試 hasRole('admin')");}}
運行
@FunctionalInterface
public interface Executer {/*** 執行*/void run();
}
...
...@SpringBootTest
public class AuthTest {@Resourceprivate AuthTestMethodService authTestService;@Resourceprivate AuthTestClassService authTestClassService;@Testvoid testInit(){AuthTestMethodService authTestService2 = new AuthTestMethodService();authTestService2.testHasRole();System.out.println("================");UserContext user = new UserContext();user.setRoles("admin,test");/* testAuth(user,()->{authTestService.testHasRole();});*/testAuth(user,()->{authTestService.testHasRole();authTestService.testHasAllRole();});user.setRoles("test");testAuth(user,()->{authTestService.testHasAnyRole();authTestService.testHasAllRole();});}@Testvoid testClass(){System.out.println("================");UserContext user = new UserContext();user.setRoles("admin,test");testAuth(user,()->{authTestClassService.testHasRole();});user.setRoles("test");testAuth(user,()->{authTestClassService.testHasRole();});}private void testAuth(UserContext user, Executer executer) {AuthContext.setUserContext(user);// 執行try{executer.run();}catch (Exception e){throw e;}finally {AuthContext.removeUserContext();}}}