当前位置: 主页 > JAVA语言

java web菜单权限控制-mvc菜单权限控制

发布时间:2023-06-04 22:05   浏览次数:次   作者:佚名

面向切面编程

面向切面编程(Aspect Oriented Programming),可以将与业务无关但是被各个业务模块共同调用的逻辑抽取出来,以切面的方式切入到代码中,从而降低系统中代码的耦合度,减少重复的代码。

Spring AOP 是通过预编译方式和运行期间动态代理实现程序面向切面编程。

试想我们的项目中有一个接口,它的代码逻辑是这样的:

public R api() {
    查询数据库;
    返回数据;
}

现在我们需要对该接口进行登录验证,只有登录了的用户才能访问该接口,如果用户没有登录,那么返回一个错误结果。此时,最简单的方式就是使用 if-else 进行判断,添加到代码逻辑中。但如果这种接口数量一多,那我们的工作量就势必加大了。

如果后续开发中,我们还需要给接口添加权限验证,只有具有某种权限的用户才能访问接口,那我们又需要添加大量重复代码。

这种应用场景,例如登录校验、权限校验、日志处理等这种多个模块可能会共同调用的代码,我们完全可以使用切面的方式,将逻辑切入到业务模块中。

AOP 的底层实现原理

AOP 底层使用动态代理完成需求,为需要增加增强功能的类生成代理类,有两种生成代理类的方式,对于被代理类(即需要增强的类)java web菜单权限控制,如果:

实现了接口,使用 JDK 动态代理,生成的代理类会使用其接口没有实现接口,使用 CGlib 动态代理,生成的代理类会继承被代理类

简单看看 JDK 动态代理的实现方式,可以看到使用了设计模式-代理模式:

// 我们定义一个接口,声明一个登录功能的方法
public interface UserService {
    void login(String username, String password);
}
// 有一个实现类,实现登录功能
public class UserServiceImpl implements UserService{
    @Override
    public void login(String username, String password) {
        System.out.println("登录功能, username="+ username + ",password=" + password);
    }
}
// 创建一个代理类,完成代理,增强被代理类的功能
public class UserServiceProxy implements InvocationHandler {
    // 被代理类的实例,传递进来的就是 UserServiceImpl 的实例
    private Object obj;
    public UserServiceProxy(Object obj) {
        this.obj = obj;
    }
    // 定义如何增强功能
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().equals("login")) {
            System.out.println("执行主体功能之前,增强功能.......");
            System.out.println("执行方法:" + method.getName() + ",方法参数: {" + Arrays.toString(args) + "}");
            // 增强功能:给用户名添加后缀,实际情况中,可能我们可以判断以下请求的 IP 地址是否在运行范围内
            args[0] += "123123";
            // 如果我们直接 return method.invoke, 不编写其他代码,那么就等于没有增强功能
            // 调用 method.invoke 就是方法执行后的返回结果,如果不调用 method.invoke,就不会执行主体功能
            Object res = method.invoke(obj, args);
            System.out.println("执行主体功能之后,增强功能.......");
            return res;
        }
        return method.invoke(obj, args);
    }
}
// 测试:
public class Main {
    public static void main(String[] args) {
        Class[] interfaces = {UserService.class};
        UserServiceImpl userServiceImpl = new UserServiceImpl();
        UserService userService = (UserService) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), interfaces,
                new UserServiceProxy(userServiceImpl));
        userService.login("username", "123123");
    }
}
//======================================= 运行结果
执行主体功能之前,增强功能.......
执行方法:login,方法参数: {[username, 123123]}
登录功能, username=username123123,password=123123
执行主体功能之后,增强功能.......

AOP 的相关术语Spring Boot 使用 Spring AOP

接下来看看在 Spring Boot 中如何使用 Spring AOP。

首先引入一个 spring-boot-starter-aop


    org.springframework.boot
    spring-boot-starter-web


    org.springframework.boot
    spring-boot-starter-aop

在实际开发中,我们可以使用切入点表达式声明切入点,如:

切入点表达式可以用上 || 、&& 逻辑运算符。

我们会使用到如下几个注解:

java web菜单权限控制_mvc菜单权限控制_java权限控制设计

我们编写两个类:

java web菜单权限控制_java权限控制设计_mvc菜单权限控制

代码如下:


//=================================== 切面代码 ===================================
@Component      // 这是一个组件,会交由 IOC 容器管理
@Aspect         // 这个类是一个切面
public class TestAOP {
    // 切入点表达式,TestController 下的 test 方法为切入点
    public static final String EXECUTION = "execution(public void com.example.aopdemo.controller.TestController.test())";
    // 也可以这样使用 @Before("execution(public void com.example.aopdemo.controller.TestController.test())")
    @Before(EXECUTION) 
    public void before() {
        System.out.println("前置通知");
    }
    // 切入点的另一种编写方式,具体使用查看第【20】行
    @Pointcut(value="execution(public void com.example.aopdemo.controller.TestController.test())")
    public void pointCut() {
    }
    @AfterReturning("pointCut()")
    public void afterReturning() {
        System.out.println("后置通知");
    }
    // ProceedingJoinPoint 实例含有切入点的信息,可以获取方法签名,参数列表等
    // 环绕通知使用这个对象实例执行切入点的功能
    @Around(EXECUTION)
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕通知之前");
        // 执行切入点
        joinPoint.proceed();
        System.out.println("环绕通知之后");
    }
    // 类似于 finally,保证一定会执行
    @After(EXECUTION)
    public void after() {
        System.out.println("最终通知");
    }
    @AfterThrowing(EXECUTION)
    public void afterThrowing() {
        System.out.println("异常通知");
    }
}
//=================================== 控制层测试代码 ===================================
@RestController
public class TestController {
    @GetMapping("/test")
    public void test() {
        // 可以查看如果发生异常,“通知”的执行顺序是怎样的
        // int i = 1/0;
        System.out.println("test 请求");
    }
}

发送一个 /test 请求,查看控制台打印结果,可以看到,各个通知的执行顺序:

## 没有异常发生的情况
环绕通知之前
前置通知
test 请求
后置通知
最终通知
环绕通知之后
## 异常发生的情况
环绕通知之前
前置通知
异常通知
最终通知
2022-08-23 14:34:06.587 ERROR 22324 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause
java.lang.ArithmeticException: / by zero

可以看到,如果发生了异常,那么切入点发生异常后续的代码、后置通知的代码、环绕通知之后的代码不会运行,并且最终通知一定会运行。

至此,Spring AOP 在 Spring Boot 如何使用已经简单介绍完毕,接下来看看如何使用 Spring AOP 实现登录鉴权。

使用注解和 Spring AOP 实现登录鉴权

假设我们有这样一个场景,某个接口需要用户具有管理员权限才能访问java web菜单权限控制,如果没有权限则抛出异常,交给全局统一异常处理。

我们可以使用拦截器完成,也可以使用自定义切面编程实现。

我们需要编写三个类:

mvc菜单权限控制_java web菜单权限控制_java权限控制设计

代码如下:


//=================================== 自定义注解代码 ===================================
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
@Component
public @interface PermissionRole {
    // role 字段声明接口需要哪种权限角色才能访问,假定我们有两种角色,普通用户,管理员(admin)
    String role() default "";
}
//=================================== 切面代码 ===================================
@Aspect
@Component
public class PermissionRoleAspect {
    // 声明一个切入点,即标注了 @PermissionRole 注解的方法
    @Pointcut("@annotation(com.example.aopdemo.annotation.PermissionRole)")
    public void check() {
    }
    // 声明切入点,这里 check() 主要是让我们获得“方法的信息”,
    //@annotation(permissionRole) 主要是让我们获得注解的信息,下面方法参数才能获取到 @PermissionRole 注解的实例信息
    @Before("check() && @annotation(permissionRole)")
    public void before(JoinPoint joinPoint, PermissionRole permissionRole) throws Exception {
        // 可以在这里获取 token,检验用户是否登录,再执行后续代码
        // 获取 @PermissionRole 中 role 字段的值
        String role = permissionRole.role();
        // 这里仅是为了方便测试,获取切入点的方法参数中携带过来信息,直接判断是否具有权限
        // 实际过程中,我们应该根据 token 得到用户信息,在根据用户信息查询数据库该用户的权限,进行判断
        for (Object arg : joinPoint.getArgs()) {
            if (arg instanceof String && arg.equals(role)) {
                System.out.println("权限验证通过");
                return;
            }
        }
        throw new Exception("当前登录用户没有操作权限");
    }
}
//=================================== 控制层测试代码 ===================================
@RestController
public class TestController {
    // 使用了自定义注解,当权限角色是 “admin” 时才能访问该接口
    @PermissionRole(role = "admin")
    @GetMapping("/permission")
    public String roleApi(String token) {
        System.out.println("token = " + token);
        return "请求通过!";
    }
}

发送请求进行测试

## 发送请求 http://localhost:8080/permission ,不携带数据,即没有权限的情况下
2022-08-23 15:02:26.058 ERROR 14404 --- [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.reflect.UndeclaredThrowableException] with root cause
java.lang.Exception: 当前登录用户没有操作权限
## 发送请求 http://localhost:8080/permission?token=admin, 携带数据,有权限的情况下
权限验证通过
token = admin

总结

在 Spring Boot 使用 Spring AOP 时,我们需要引入一个 spring-boot-starter-aop ,就可以进行切面编程。

我们需要了解几个常用注解的用法:

在声明切入点的时候,我们可以使用切入点表达式声明切入点。

此外,如果有多个切面,可以在切面类上使用注解 @Order 声明优先级,值越小优先级越高,越先执行。