【SpringBoot应用篇】【AOP+注解】SpringBoot+SpEL表达式基于注解实现权限控制

【SpringBoot应用篇】【AOP+注解】SpringBoot+SpEL表达式基于注解实现权限控制

Spring SpEL

Spring 表达式语言 SpEL 是一种非常强大的表达式语言,它支持在运行时查询和操作对象图。 它提供了许多高级功能,例如方法调用和基本的字符串模板功能。表达式语言给静态Java语言增加了动态功能。

Spring 表达式语言最初是为 Spring 社区创建的,它拥有一种受良好支持的表达式语言,可用于 Spring 产品组合中的所有产品。 虽然 SpEL 是 Spring 产品组合中表达式评估的基础,但它不直接与 Spring 绑定,可以独立使用

Spring Security框架中启用prePost注解的支持就是使用SpEL表达式实现的权限控制

  • @PreAuthorize("hasAuthority('save')")
  • @PreAuthorize("isAnonymous()")
  • @PreAuthorize("isAuthenticated()")

SpEL支持如下表达式:

  • 基本表达式: 字面量表达式、关系,逻辑与算数运算表达式、字符串连接及截取表达式、三目运算及Elivis表达式、正则表达式、括号优先级表达式
  • 类相关表达式: 类类型表达式、类实例化、instanceof表达式、变量定义及引用、赋值表达式、自定义函数、对象属性存取及安全导航表达式、对象方法调用、Bean引用
  • 集合相关表达式:内联List、内联数组、集合,字典访问、列表,字典,数组修改、集合投影、集合选择;不支持多维内联数组初始化;不支持内联字典定义
  • 其他表达式:模板表达式。

注:SpEL表达式中的关键字是不区分大小写的。

基本表达式

java 复制代码
/**
* 基本表达式 : 字面量表达式
*/
@Test
public void test01() {
    // 1)创建解析器:SpEL使用ExpressionParser接口表示解析器,提供SpelExpressionParser默认实现;
    ExpressionParser parser = new SpelExpressionParser();
    // 2)解析表达式:使用ExpressionParser的parseExpression来解析相应的表达式为Expression对象。
    Expression expression = parser.parseExpression("1+2");
    // 3)求值:通过Expression接口的getValue方法根据上下文获得表达式值。
    System.out.println(expression.getValue());



    String str1 = parser.parseExpression("'Hello World!'").getValue(String.class);
    int int1 = parser.parseExpression("1").getValue(Integer.class);
    long long1 = parser.parseExpression("-1L").getValue(long.class);
    float float1 = parser.parseExpression("1.1").getValue(Float.class);
    double double1 = parser.parseExpression("1.1E+2").getValue(double.class);
    int hex1 = parser.parseExpression("0xa").getValue(Integer.class);
    long hex2 = parser.parseExpression("0xaL").getValue(long.class);
    boolean true1 = parser.parseExpression("true").getValue(boolean.class);
    boolean false1 = parser.parseExpression("false").getValue(boolean.class);
    Object null1 = parser.parseExpression("null").getValue(Object.class);

    System.out.println("str1=" + str1);
    System.out.println("int1=" + int1);
    System.out.println("long1=" + long1);
    System.out.println("float1=" + float1);
    System.out.println("double1=" + double1);
    System.out.println("hex1=" + hex1);
    System.out.println("hex2=" + hex2);
    System.out.println("true1=" + true1);
    System.out.println("false1=" + false1);
    System.out.println("null1=" + null1);
}
java 复制代码
/**
* 基本表达式 : 字面量表达式
*/
@Test
public void test02() {
    // 1)创建解析器:SpEL使用ExpressionParser接口表示解析器,提供SpelExpressionParser默认实现;
    ExpressionParser parser = new SpelExpressionParser();
    // 2)解析表达式:使用ExpressionParser的parseExpression来解析相应的表达式为Expression对象。
    Expression expression = parser.parseExpression("('Hello' + ' World').concat(#end)");
    // 3)构造上下文:准备比如变量定义等等表达式需要的上下文数据。
    EvaluationContext context = new StandardEvaluationContext();
    context.setVariable("end", "!");
    // 4)求值:通过Expression接口的getValue方法根据上下文获得表达式值。
    System.out.println(expression.getValue(context));
}

类相关表达式

java 复制代码
@Data
@Component("ss")
public class User {
    private String username;
    private String address;
    private Integer age;


    public String sayHello(String username) {
        return "hello " + username;
    }

    public String sayHello(Integer age) {
        return "hello " + username + ";age=" + age;
    }
    public String sayHello() {
        return "hello " + username;
    }
}
java 复制代码
/**
* 类相关表达式: 变量定义及引用
*/
@Test
public void test03() {
    String expression = "#user.username";
    ExpressionParser parser = new SpelExpressionParser();
    Expression exp = parser.parseExpression(expression);
    StandardEvaluationContext ctx = new StandardEvaluationContext();
    User user = new User();
    user.setAddress("长沙");
    user.setUsername("zysheep");
    user.setAge(24);;

    ctx.setVariable("user", user);

    String value = exp.getValue(ctx, String.class);
    System.out.println("value = " + value);
}
java 复制代码
@Test
public void test04() {
    String expression = "username";
    ExpressionParser parser = new SpelExpressionParser();
    Expression exp = parser.parseExpression(expression);
    StandardEvaluationContext ctx = new StandardEvaluationContext();
    User user = new User();
    user.setAddress("长沙");
    user.setUsername("zysheep");
    user.setAge(24);

    // user 对象设置为 rootObject,那么表达式中就不需要 #user.
    ctx.setRootObject(user);

    String value = exp.getValue(ctx, String.class);
    System.out.println("value = " + value);
}
java 复制代码
/**
* 类相关表达式: 对象方法调用
*/
@Test
public void test05() {
    String expression = "sayHello(99)";
    ExpressionParser parser = new SpelExpressionParser();
    Expression exp = parser.parseExpression(expression);

    StandardEvaluationContext ctx = new StandardEvaluationContext();

    User user = new User();
    user.setAddress("长沙");
    user.setUsername("zysheep");
    user.setAge(24);
    ctx.setRootObject(user);

    String value = exp.getValue(ctx, String.class);
    System.out.println("value = " + value);
}
java 复制代码
/**
* 类相关表达式: 对象方法调用
*
* 调用无参的 sayHello
*/
@Test
public void test06() {
    String expression = "sayHello()";
    ExpressionParser parser = new SpelExpressionParser();
    Expression exp = parser.parseExpression(expression);

    StandardEvaluationContext ctx = new StandardEvaluationContext();

    User user = new User();
    user.setAddress("长沙");
    user.setUsername("zysheep");
    user.setAge(24);
    ctx.setRootObject(user);

    String value = exp.getValue(ctx, String.class);
    System.out.println("value = " + value);
}
java 复制代码
/**
* 类相关表达式: Bean引用
*/
@Test
public void test07() {
    // 通过 SpEL 表达式来调用这个名为 ss 的 bean 中的 sayHello 方法
    String expression = "@ss.sayHello('spel bean引用')";
    ExpressionParser parser = new SpelExpressionParser();
    Expression exp = parser.parseExpression(expression);
    StandardEvaluationContext ctx = new StandardEvaluationContext();
    // 给配置的上下文环境设置一个 bean 解析器,这个 bean 解析器会自动跟进名字从 Spring 容器中找打响应的 bean 并执行对应的方法
    ctx.setBeanResolver(new BeanFactoryResolver(beanFactory));
    String value = exp.getValue(ctx, String.class);
    System.out.println("value = " + value);
}

表达式模板

java 复制代码
/**
* 表达式模板:
* 模板表达式就是由字面量与一个或多个表达式块组成。每个表达式块由"前缀+表达式+后缀"形式组成,如"${1+2}"即表达式块
*
*/
@Test
public void test08() {
    //创建解析器
    SpelExpressionParser parser = new SpelExpressionParser();
    //创建解析器上下文
    ParserContext context = new TemplateParserContext("%{", "}");
    Expression expression = parser.parseExpression("你好:%{#name},我们正在学习:%{#lesson}", context);
    //创建表达式计算上下文
    EvaluationContext evaluationContext = new StandardEvaluationContext();
    evaluationContext.setVariable("name", "zysheep");
    evaluationContext.setVariable("lesson", "SpEL表达式!");
    //获取值
    String value = expression.getValue(evaluationContext, String.class);
    System.out.println(value);
}

SpEL表达式实现权限控制

PreAuth

java 复制代码
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PreAuth {

    /**
     *
     *
     * permissionAll()-----只要配置了角色就可以访问
     * hasPermission("MENU.QUERY")-----有MENU.QUERY操作权限的角色可以访问
     * permitAll()-----放行所有请求
     * denyAll()-----只有超级管理员角色才可访问
     * hasAuth()-----只有登录后才可访问
     * hasTimeAuth(1,,10)-----只有在1-10点间访问
     * hasRole('管理员')-----具有管理员角色的人才能访问
     * hasAllRole('管理员','总工程师')-----同时具有管理员、总工程师角色的人才能访问
     *
     * Spring el
     * 文档地址:https://docs.spring.io/spring/docs/5.1.6.RELEASE/spring-framework-reference/core.html#expressions
     */
    String value();

}

AuthFun

java 复制代码
/**
 * <p>
 * 类相关表达式: Bean引用,参数要用单引号包括
 *
 * @PreAuth("@af.hasPermission('ADMIN, USER')")
 * </p>
 *
 * @author : lyw
 * @since : 2023/11/23 16:01
 */
@Component("af")
public class AuthFun {


    /**
     * 判断角色是否具有接口权限
     *
     * @return {boolean}
     */
    public boolean permissionAll() {
        //TODO  读取数据库权限数据
        return true;
    }

    /**
     * 判断角色是否具有接口权限
     *
     * @param permission 权限编号,对应菜单的MENU_CODE
     * @return {boolean}
     */
    public boolean hasPermission(String permission) {
      return hasRole(permission);
    }

    /**
     * 放行所有请求
     *
     * @return {boolean}
     */
    public boolean permitAll() {
        return true;
    }

    /**
     * 只有超管角色才可访问
     *
     * @return {boolean}
     */
    public boolean denyAll() {
        return hasRole("ADMIN");
    }

    /**
     * 是否已授权
     *
     * @return {boolean}
     */
    public boolean hasAuth() {

        return true;
    }

    /**
     * 是否有时间授权
     *
     * @param start 开始时间
     * @param end   结束时间
     * @return {boolean}
     */
    public boolean hasTimeAuth(Integer start, Integer end) {
        Integer hour = DateUtil.hour(new Date(), true);
        return hour >= start && hour <= end;
    }

    /**
     * 判断是否有该角色权限
     *
     * @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 role 角色集合
     * @return {boolean}
     */
    public boolean hasAnyRole(String... role) {
        return Arrays.stream(role).anyMatch(item -> StringUtils.equals("ADMIN", item));
    }
}

PreAuthAspect

java 复制代码
@Aspect
@Component
public class PreAuthAspect {

    @Autowired
    private BeanFactory applicationContext;

    private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();


    @Pointcut("@annotation(cn.zysheep.annotation.PreAuth) || @within(cn.zysheep.annotation.PreAuth)")
    public void pointcut() {}


    @Around("pointcut()")
    public Object preAuth(ProceedingJoinPoint point) throws Throwable {
        if (handleAuth(point)) {
            return point.proceed();
        }
        throw new BizException("用户无权限,非法用户!");
    }


    private boolean handleAuth(ProceedingJoinPoint point) {
        MethodSignature ms = point.getSignature() instanceof MethodSignature? (MethodSignature) point.getSignature():null;
        Method method = ms.getMethod();
        // 读取权限注解,优先方法上,没有则读取类
        PreAuth preAuth = method.getAnnotation( PreAuth.class);
        // 判断表达式
        String condition = preAuth.value();
        if (StringUtils.isNotBlank(condition)) {
            // @PreAuth("@af.hasPermission('ADMIN, USER')")
            Expression expression = EXPRESSION_PARSER.parseExpression(condition);
            // 方法参数值
            Object[] args = point.getArgs();
            StandardEvaluationContext context = getEvaluationContext(method, args);
            // 获取解析计算的结果
            return expression.getValue(context, Boolean.class);
        }
        return false;
    }
    /**
     * 获取方法上的参数
     *
     * @param method 方法
     * @param args   变量
     * @return {SimpleEvaluationContext}
     */
    private StandardEvaluationContext getEvaluationContext(Method method, Object[] args) {
        // 初始化Spel表达式上下文,并设置 AuthFun
        StandardEvaluationContext context = new StandardEvaluationContext();
        // 设置表达式支持spring bean
        context.setBeanResolver(new BeanFactoryResolver(applicationContext));

        // 可以从session中获取登录用户的权限
//        for (int i = 0; i < args.length; i++) {
//            // 读取方法参数
//            MethodParameter methodParam = ClassUtil.getMethodParameter(method, i);
//            // 设置方法 参数名和值 为spel变量
//            context.setVariable(methodParam.getParameterName(), args[i]);
//        }
        return context;
    }
}

UserController

java 复制代码
@RestController
@RequestMapping("/api")
@Slf4j
public class UserController {


    @GetMapping("/save")
    @PreAuth("@af.hasPermission('ADMIN')")
    public R save() {
        log.info("====执行保存业务逻辑=====");
        return R.success();
    }

    @GetMapping("/get")
    @PreAuth("@af.hasPermission('USER')")
    public R get() {
        log.info("====执行保存业务逻辑=====");
        return R.success();
    }

}

SpelParserUtils

java 复制代码
public final class SpelParserUtils {

    private static final String EXPRESSION_PREFIX = "#{";

    private static final String EXPRESSION_SUFFIX = "}";

    /**
     * 表达式解析器
     */
    private static ExpressionParser expressionParser = new SpelExpressionParser();

    /**
     *  参数名解析器,用于获取参数名
     */
    private static DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();



    private SpelParserUtils(){}

    /**
     * 解析spel表达式
     *
     * @param method 方法
     * @param args 参数值
     * @param spelExpression  表达式
     * @param clz  返回结果的类型
     * @param defaultResult 默认结果
     * @return 执行spel表达式后的结果
     */
    public static <T> T parse(Method method, Object[] args, String spelExpression, Class<T> clz, T defaultResult) {
        String[] params = parameterNameDiscoverer.getParameterNames(method);
        EvaluationContext context = new StandardEvaluationContext();
        //设置上下文变量
        for (int i = 0; i < params.length; i++) {
            context.setVariable(params[i], args[i]);
        }
        T result = getResult(context,spelExpression,clz);
        if(Objects.isNull(result)){
            return defaultResult;
        }
        return result;
    }

    /**
     * 解析spel表达式
     *
     * @param method  方法
     * @param args 参数值
     * @param spelExpression  表达式
     * @param clz  返回结果的类型
     * @return 执行spel表达式后的结果
     */
    public static <T> T parse(Method method, Object[] args, String spelExpression, Class<T> clz) {
        String[] params = parameterNameDiscoverer.getParameterNames(method);
        EvaluationContext context = new StandardEvaluationContext();
        //设置上下文变量
        for (int i = 0; i < params.length; i++) {
            context.setVariable(params[i], args[i]);
        }
        return getResult(context,spelExpression,clz);
    }

    /**
     * 解析spel表达式
     *
     * @param param  参数名
     * @param paramValue 参数值
     * @param spelExpression  表达式
     * @param clz  返回结果的类型
     * @return 执行spel表达式后的结果
     */
    public static <T> T parse(String param, Object paramValue, String spelExpression, Class<T> clz) {
        EvaluationContext context = new StandardEvaluationContext();
        //设置上下文变量
        context.setVariable(param, paramValue);
        return getResult(context,spelExpression,clz);
    }


    /**
     * 解析spel表达式
     *
     * @param param 参数名
     * @param paramValue 参数值
     * @param spelExpression  表达式
     * @param clz  返回结果的类型
     * @param defaultResult 默认结果
     * @return 执行spel表达式后的结果
     */
    public static <T> T parse(String param, Object paramValue,String spelExpression, Class<T> clz, T defaultResult) {
        EvaluationContext context = new StandardEvaluationContext();
        //设置上下文变量
        context.setVariable(param, paramValue);
        T result = getResult(context,spelExpression,clz);
        if(Objects.isNull(result)){
            return defaultResult;
        }
        return result;

    }


    /**
     * 获取spel表达式后的结果
     *
     * @param context 解析器上下文接口
     * @param spelExpression  表达式
     * @param clz  返回结果的类型
     * @return 执行spel表达式后的结果
     */
    private static <T> T getResult(EvaluationContext context,String spelExpression, Class<T> clz){
        try {
            //解析表达式
            Expression expression = parseExpression(spelExpression);
            //获取表达式的值
            return expression.getValue(context, clz);
        } catch (Exception e) {
            log.error(e.getMessage(),e);
        }
        return null;
    }


    /**
     * 解析表达式
     * @param spelExpression spel表达式
     * @return
     */
    private static Expression parseExpression(String spelExpression){
        // 如果表达式是一个#{}表达式,需要为解析传入模板解析器上下文
        if(spelExpression.startsWith(EXPRESSION_PREFIX) && spelExpression.endsWith(EXPRESSION_SUFFIX)){
            return expressionParser.parseExpression(spelExpression,new TemplateParserContext());
        }

        return expressionParser.parseExpression(spelExpression);
    }

}
相关推荐
weixin_462428473 分钟前
使用 Caffeine 缓存并在业务方法上通过注解实现每3到5秒更新缓存
java·缓存
程序媛小果4 分钟前
基于java+SpringBoot+Vue的桂林旅游景点导游平台设计与实现
java·vue.js·spring boot
骑鱼过海的猫1236 分钟前
【java】java通过s3访问ceph报错
java·ceph·iphone
杨充12 分钟前
13.观察者模式设计思想
java·redis·观察者模式
Lizhihao_14 分钟前
JAVA-队列
java·开发语言
喵叔哟23 分钟前
重构代码之移动字段
java·数据库·重构
喵叔哟24 分钟前
重构代码之取消临时字段
java·前端·重构
fa_lsyk26 分钟前
maven环境搭建
java·maven
Daniel 大东1 小时前
idea 解决缓存损坏问题
java·缓存·intellij-idea
2401_857636391 小时前
共享汽车管理新纪元:SpringBoot框架应用
数据库·spring boot·汽车