找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

635

积分

0

好友

79

主题
发表于 前天 23:50 | 查看: 0| 回复: 0

Spring Boot 应用开发中,利用自定义注解结合切面(AOP)来实现接口权限控制是一种常见做法。通常的实现思路包含以下几个步骤:

  1. 自定义一个权限校验的注解,包含参数 value。
  2. 在对应的接口方法上配置该注解。
  3. 定义一个切面类,并指定切点。
  4. 在切入的方法体内编写权限判断的逻辑。

这套流程看似清晰易懂,但在实际复杂的业务场景中,我们往往会遇到多样化的权限校验需求。例如:

  • 只要用户配置了任何角色,就可以访问。
  • 用户拥有某个特定操作权限才可以访问。
  • 无条件放行所有请求。
  • 只有超级管理员角色才可以访问。
  • 用户登录后才可以访问。
  • 在指定的时间段内允许访问。
  • 用户拥有某个特定角色才可以访问。
  • 用户必须同时具有多个指定角色才可以访问。

面对如此多的场景,如果按照传统方式为每种情况都编写独立的注解和判断逻辑,代码将变得臃肿且难以维护。此时,SpEL(Spring Expression Language)表达式就为我们提供了一种更为优雅和灵活的解决方案。

什么是 SpEL 表达式?

SpEL 的全称为 Spring Expression Language,即 Spring 表达式语言,自 Spring 3.0 开始提供。它最强大的功能在于,能够在运行时动态地执行表达式,并将结果装配到属性或构造函数中。简单来说,它允许我们将权限规则以字符串表达式的形式动态配置,并由框架在运行时解析执行,从而实现权限逻辑与业务代码的解耦。

实现步骤详解

1. 自定义注解

首先,我们需要定义一个用于权限控制的注解。与传统做法不同,这里的 value 属性将用于接收 SpEL 表达式字符串。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PreAuth {

    /**
     * Spring EL 表达式,用于动态权限校验。
     * 示例:
     *   hasPermission('MENU.QUERY')         - 有 MENU.QUERY 操作权限的角色可以访问
     *   hasRole('管理员')                    - 具有管理员角色的人才能访问
     *   hasAllRole('管理员','总工程师')      - 同时具有管理员、总工程师角色的人才能访问
     *   permitAll()                         - 放行所有请求
     *   denyAll()                          - 只有超级管理员角色才可访问
     *   hasAuth()                          - 只有登录后才可访问
     *   hasTimeAuth(1, 10)                 - 只有在1-10点间可以访问
     */
    String value();
}

2. 定义切面与切入点

接下来,我们定义切面。这里的关键是切入点的选择:我们希望注解既可以作用于方法上,也可以作用于类上(此时对该类下所有方法生效)。因此,我们使用 @annotation@within 来定义组合切点。

@Around("@annotation(com.yourpackage.PreAuth) || @within(com.yourpackage.PreAuth)")
public Object preAuth(ProceedingJoinPoint point) throws Throwable {
    if (handleAuth(point)) {
        return point.proceed();
    }
    throw new SecureException(ResultCode.REQ_REJECT);
}

private boolean handleAuth(ProceedingJoinPoint point) {
    // TODO: 权限校验逻辑,返回 true 或 false
}

3. 集成 SpEL 进行权限校验

这是最核心的部分。我们将在 handleAuth 方法中,解析注解上的 SpEL 表达式并执行。

3.1 引入 SpEL 解析器

private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();

3.2 表达式解析与执行

private boolean handleAuth(ProceedingJoinPoint point) {
    MethodSignature ms = point.getSignature() instanceof MethodSignature ? (MethodSignature) point.getSignature() : null;
    Method method = ms.getMethod();
    // 读取权限注解,优先从方法上读取,没有则读取类上的注解
    PreAuth preAuth = ClassUtil.getAnnotation(method, PreAuth.class);
    // 获取注解中的表达式
    String condition = preAuth.value();
    if (StringUtil.isNotBlank(condition)) {
        // 解析表达式
        Expression expression = EXPRESSION_PARSER.parseExpression(condition);
        // 获取方法参数值
        Object[] args = point.getArgs();
        // 创建并设置表达式执行上下文
        StandardEvaluationContext context = getEvaluationContext(method, args);
        // 执行表达式并获取布尔值结果
        return expression.getValue(context, Boolean.class);
    }
    return false;
}

/**
 * 构建 SpEL 表达式执行上下文
 * @param method 当前执行的方法
 * @param args   方法参数
 * @return StandardEvaluationContext
 */
private StandardEvaluationContext getEvaluationContext(Method method, Object[] args) {
    // 初始化上下文,并注册自定义的权限校验函数类 AuthFun
    StandardEvaluationContext context = new StandardEvaluationContext(new AuthFun());
    // 设置上下文支持解析 Spring Bean(如果表达式需要引用Bean)
    context.setBeanResolver(new BeanFactoryResolver(applicationContext));
    // 将方法参数设置为SpEL上下文中的变量,便于在表达式中使用
    for (int i = 0; i < args.length; i++) {
        MethodParameter methodParam = ClassUtil.getMethodParameter(method, i);
        context.setVariable(methodParam.getParameterName(), args[i]);
    }
    return context;
}

4. 实现权限校验逻辑类 AuthFun

请注意 getEvaluationContext 方法中的 new AuthFun(),这个 AuthFun 类是我们的“魔法”所在。该类中定义的每一个公共方法,都可以直接在 @PreAuth 注解的表达式里调用。这里是具体权限逻辑实现的地方。

public class AuthFun {

    /**
     * 判断角色是否具有接口权限 (示例逻辑,需按需实现)
     */
    public boolean permissionAll() {
        //TODO: 实现“有任意角色即可访问”的逻辑
        return true;
    }

    /**
     * 判断是否具有特定操作权限
     * @param permission 权限编号
     */
    public boolean hasPermission(String permission) {
        //TODO: 实现根据权限编码校验的逻辑
        return true;
    }

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

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

    /**
     * 是否已登录授权
     */
    public boolean hasAuth() {
        if(Func.isEmpty(AuthUtil.getUser())){
            // TODO 返回异常或 false
            return false;
        } else {
            return true;
        }
    }

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

    /**
     * 判断是否有该角色权限
     * @param role 角色名
     */
    public boolean hasRole(String role) {
        return hasAnyRole(role);
    }

    /**
     * 判断是否具有所有指定角色权限
     * @param role 角色集合
     */
    public boolean hasAllRole(String... role) {
        for (String r : role) {
            if (!hasRole(r)) {
                return false;
            }
        }
        return true;
    }

    /**
     * 判断是否具有任意一个指定角色权限
     * @param role 角色集合
     */
    public boolean hasAnyRole(String... role) {
        // 获取当前登录用户(示例,需替换为实际获取方式)
        BladeUser user = AuthUtil.getUser();
        if (user == null) {
            return false;
        }
        String userRole = user.getRoleName();
        if (StringUtil.isBlank(userRole)) {
            return false;
        }
        String[] roles = Func.toStrArray(userRole);
        for (String r : role) {
            if (CollectionUtil.contains(roles, r)) {
                return true;
            }
        }
        return false;
    }
}

实际应用

Controller 层使用时,我们只需在类或方法上添加 @PreAuth 注解,并在 value 中写入符合 SpEL 语法的表达式即可。表达式调用的正是 AuthFun 类中定义的方法。

例如,要求用户同时具有“管理员”和“总工程师”角色:

@PreAuth("hasAllRole('管理员','总工程师')")
@GetMapping("/some-api")
public ResponseEntity<?> someApi() {
    // ...
}

要求用户拥有 LM_QUERYLM_QUERY_ALL 权限之一:

@PreAuth("hasPermission('LM_QUERY') or hasPermission('LM_QUERY_ALL')")
public T queryMethod(...) {
    // ...
}

实现原理简述

整个流程的核心在于 SpEL 的动态解析能力。Spring AOP 拦截到带有 @PreAuth 注解的方法调用后,切面会提取注解中的表达式字符串(如 "hasAllRole('管理员','总工程师')")。SpEL 解析器会将其解析为一个可执行的表达式对象。

随后,解析器在指定的上下文(StandardEvaluationContext)中执行该表达式。这个上下文里注册了我们自定义的 AuthFun 对象。因此,当解析到 hasAllRole 时,它会自动调用 AuthFun.hasAllRole(String... role) 方法,并将字符串参数 '管理员''总工程师' 传递进去,最终根据该方法的返回值决定是否放行请求。

总结与优势

通过引入 SpEL 表达式,我们将原本硬编码在切面或注解中的权限判断逻辑,抽象为可动态配置的字符串规则。这种设计模式带来的主要优势包括:

  • 高度灵活:新的权限校验场景只需在 AuthFun 类中添加对应方法,并在注解中配置相应表达式即可,无需修改切面核心逻辑或创建新注解。
  • 高度可读:权限规则以接近自然语言的表达式形式直观地写在注解中,一目了然。
  • 便于维护:权限逻辑集中在 AuthFun 类中,结构清晰,易于管理和扩展。
  • 强大表达能力SpEL 本身支持运算符、条件判断、方法调用等,可以组合出非常复杂的权限规则。

这种基于 Spring BootSpEL 的权限控制方案,优雅地解决了复杂多变的权限需求,是构建灵活、可扩展的后端权限系统的优秀实践。如果你想了解更多关于 Spring BootAOP 的进阶用法,可以到云栈社区Java技术板块与其他开发者交流探讨。




上一篇:Block 开源 Goose:这不仅仅是 AI 助手,更是终端里的“初级 SRE”
下一篇:基于树莓派CM0的车道检测与车道保持系统实现
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-24 01:45 , Processed in 0.497596 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表