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

news2024/11/24 11:04:08

目录

  • 参考
  • 一、概念
    • 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 {

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

    }

}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/876864.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LVGL学习笔记 30 - List(列表)

目录 1. 添加文本 2. 添加按钮 3. 事件 4. 修改样式 4.1 背景色 4.2 改变项的颜色 列表是一个垂直布局的矩形&#xff0c;可以向其中添加按钮和文本。 lv_obj_t* list1 lv_list_create(lv_scr_act());lv_obj_set_size(list1, 180, 220);lv_obj_center(list1); 部件包含&…

Linux命令200例:adduser用于创建新用户

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;全栈领域新星创作者✌。CSDN专家博主&#xff0c;阿里云社区专家博主&#xff0c;2023年6月csdn上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师&#xff0c;项目技术负责人。 &…

求Win11系统virtualbox+vagrant安装MacOS虚拟机

文章目录 一、背景二、素材2.1、virtualboxvagrant 三、问题3.1、安装失败3.2、第二个失败3.3、网络说 四、求助 一、背景 题主&#xff0c;主要是穷&#xff0c;没钱买mac笔记本或相关系统的苹果产品&#xff0c;哈哈&#xff0c;偶尔也有用过MacOS系统&#xff0c;只是还没有…

app专项测试:app弱网测试

目录 弱网测试背景 网络测试要点 弱网测试关注指标 弱网测试工具 fiddler模拟网络延时场景 网络设置参考 Network Emulator Toolkit模拟网络丢包场景&#xff08;windows网络&#xff09; APP弱网测试 弱网使用工具&#xff1a; app弱网测试要点 APP网络测试要点 网络…

OpenCV图像处理——模版匹配和霍夫变换

目录 模版匹配原理实现 霍夫变换霍夫线检测 模版匹配 原理 实现 rescv.matchTemplate(img,template,method)import numpy as np import cv2 as cv import matplotlib.pyplot as pltimgcv.imread(./汪学长的随堂资料/6/模板匹配/lena.jpg) templatecv.imread(./汪学长的随堂资…

一文搞懂Jenkins持续集成解决的是什么问题

1、持续集成的定义 大师 Martin Fowler 是这样定义持续集成的: 持续集成是一种软件开发实战, 即团队开发成员经常集成他们的工作. 通常, 每个成员每天至少集成一次, 也就意味着每天可能发生多次集成. 持续集成并不能消除Bug, 而是让它们非常容易发现和改正. 根据对项目实战的…

uniapp开发小程序-有分类和列表时,进入页面默认选中第一个分类

一、效果&#xff1a; 如下图所示&#xff0c;进入该页面后&#xff0c;默认选中第一个分类&#xff0c;以及第一个分类下的列表数据。 二、代码实现&#xff1a; 关键代码&#xff1a; 进入页面时&#xff0c;默认调用分类的接口&#xff0c;在分类接口里做判断&#xff…

Android 网络编程-网络请求

Android 网络编程-网络请求 文章目录 Android 网络编程-网络请求一、主要内容二、开发网络请求前的基本准备1、查看需要请求的网址是否有效&#xff08;1&#xff09;通过网页在线验证&#xff08;2&#xff09;使用专用window网咯请求工具&#xff08;3&#xff09;编写app代码…

less学习语法

1.CSS函数的补充 1.rgb/rgba/translate/rotate/scale 2.非常好用的css函数&#xff1a; var:使用css定义的变量calc:计算css值&#xff0c;通常用于计算元素的大小或位置blur:毛玻璃&#xff08;高斯模糊&#xff09;效果gradient:颜色渐变函数 var:定义变量 css中可以自定…

关于青少年学习演讲与口才对未来的领导力的塑造的探析

标题&#xff1a;青少年学习演讲与口才对未来领导力的塑造&#xff1a;一项探析 摘要&#xff1a; 本论文旨在探讨青少年学习演讲与口才对未来领导力的塑造的重要性和影响。通过分析演讲和口才对青少年的益处&#xff0c;以及如何培养这些技能来促进领导力的发展&#xff0c;我…

pytest数据驱动 pandas

pytest数据驱动 pandas 主要过程&#xff1a;用pandas读取excel里面的数据&#xff0c;然后进行百度查询&#xff0c;并断言 pf pd.read_excel(data_py.xlsx, usecols[1,2])print(pf.values)输出&#xff1a;[[‘听妈妈的话’ ‘周杰伦’] [‘遇见’ ‘孙燕姿’] [‘伤心太平…

win10下如何安装ffmpeg

安装ffmpeg之前先安装win10 绿色软件管理软件&#xff1a;scoop. Scoop的基本介绍 Scoop是一款适用于Windows平台的命令行软件&#xff08;包&#xff09;管理工具&#xff0c;这里是Github介绍页。简单来说&#xff0c;就是可以通过命令行工具&#xff08;PowerShell、CMD等…

性能优化-超大图加载

超大图加载优化 网站首页的头部有时候要加载超大图&#xff0c;可能超过4M&#xff0c;如果网速不好用户会看到好长时间的白屏&#xff0c;体验非常不好。 这里使用大图缩略图的模式处理这个问题&#xff1a; 使用工具根据大图(>1M)做一个10k的缩略图图片同时加载&#xf…

这所院校性价比极高!专业课太简单了!保护一志愿!

一、学校及专业介绍 昆明理工大学&#xff08;Kunming University of Science and Technology&#xff09;&#xff0c;位于云南省昆明市&#xff0c;是国防科技工业局与云南省共建的重点大学&#xff0c;国家"特色重点学科项目"建设高校。昆工就综合实力和竞争压力…

Vue 引入 Element-UI 组件库

Element-UI 官网地址&#xff1a;https://element.eleme.cn/#/zh-CN 完整引入&#xff1a;会将全部组件打包到项目中&#xff0c;导致项目过大&#xff0c;首次加载时间过长。 下载 Element-UI 一、打开项目&#xff0c;安装 Element-UI 组件库。 使用命令&#xff1a; npm …

Camx--概述

该部分代码主要位于 vendor/qcom/proprietary/ 目录下&#xff1a; 其中 camx 代表了通用功能性接口的代码实现集合&#xff08;CamX&#xff09;&#xff0c;chi-cdk代表了可定制化需求的代码实现集合&#xff08;CHI&#xff09;&#xff0c;从图中可以看出Camx部分对上作为H…

开源项目-高校自动排课系统

哈喽,大家好,今天给大家带来一个开源项目-基于遗传算法的一个高校自动排课系统,同时也是一个前后端分离项目。 前端:React 后端:SpringBoot+MyBatis+MySQL数据库 高校自动排课系统的主要功能包括查询模块,安排教学区域,排课页面三部分 登录 查询模块 可以根据学年…

docker pull 设置代理 centos

On CentOS the configuration file for Docker is at: /etc/sysconfig/docker 用 root 权限打开 text editor sudo gedit 注意 加引号 Adding the below line helped me to get the Docker daemon working behind a proxy server: HTTP_PROXY“http://<proxy_host>:&…

学习笔记整理-DOM-01-基础知识

一、DOM基本概念 1. DOM基本概念 DOM是JS操控HTML和CSS的桥梁。DOM是JS操作HTML变得优雅。 DOM(Document Object Model&#xff0c;文档对象模型)是JavaScript操作HTML文档的接口&#xff0c;使文档操作变得非常优雅、简便。DOM最大的特点就是将文档表示为节点树。 节点的node…