使用SpringAop切面编程通过Spel表达式实现Controller权限控制

参考

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 {

    @Resource
    private AuthContext authContext;

    @PostConstruct
    public 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) {
        //TODO
        return hasAnyPermission(permission);
    }

    /**
     * 判断角色是否具有接口权限
     *
     * @param permission 权限编号,对应菜单的MENU_CODE
     * @return {boolean}
     */
    public boolean hasAllPermission(String... permission) {
        //TODO
        for (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 {

    @Resource
    private AuthTestMethodService authTestService;
    @Resource
    private AuthTestClassService authTestClassService;
    @Test
    void 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();
        });
    }

    @Test
    void 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();
        }

    }

}

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring AOP中,可以使用切面编程实现动态数据源的切换。下面是一个简单的示例: 首先,创建一个注解类`DataSource`,用于标识需要切换数据源的方法: ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface DataSource { String value() default "default"; } ``` 然后,创建一个切面类`DataSourceAspect`,在该类中定义切点和切面逻辑: ```java @Aspect @Component public class DataSourceAspect { @Around("@annotation(dataSource)") public Object switchDataSource(ProceedingJoinPoint joinPoint, DataSource dataSource) throws Throwable { try { // 获取要切换的数据源名称 String dataSourceName = dataSource.value(); // 根据数据源名称切换数据源 switchDataSource(dataSourceName); // 执行目标方法 return joinPoint.proceed(); } finally { // 切换回默认数据源 switchDataSource("default"); } } // 实际切换数据源的逻辑 private void switchDataSource(String dataSourceName) { // 根据传入的数据源名称进行数据源切换逻辑的实现 // ... } } ``` 在上述代码中,`@Around("@annotation(dataSource)")`表示拦截带有`@DataSource`注解的方法,并执行切面逻辑。在切面逻辑中,首先获取切换的数据源名称,然后根据该名称进行数据源的切换操作。在目标方法执行完毕后,切面逻辑会将数据源切换回默认的数据源。 最后,使用`@DataSource`注解标识需要切换数据源的方法: ```java @Service public class UserService { @DataSource("db1") public void addUser(User user) { // 执行添加用户的逻辑 } @DataSource("db2") public void updateUser(User user) { // 执行更新用户的逻辑 } } ``` 在上述示例中,`addUser`方法使用名为"db1"的数据源,`updateUser`方法使用名为"db2"的数据源。 通过以上步骤,就可以使用Spring AOP实现动态数据源的切换。当调用带有`@DataSource`注解的方法时,切面会根据注解中指定的数据源名称进行数据源切换,从而实现动态切换数据源的效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值