Shiro结合SpEL实现细粒度权限校验笔记

参考文章

https://www.cnblogs.com/felixwu0525/p/11482419.html

(代码大部分来源于此)

前言

我们遇到的问题是什么?

假设一个简单模型:超级管理员创建了一个 “部门” 对象,部门名下有部门成员(用户),部门成员可以查询本部门的人员名单;部门成员中有部门管理员,可以邀请和移除部门成员

那么按照常规的Shiro权限字符串的构建规则,这里涉及到的权限字符串应当为:

  • 部门成员:邀请:${部门UUID}
  • 部门成员:移除:${部门UUID}
  • 部门成员:查询:${部门UUID}

其中 部门UUID 是一个变量。而我们授权操作是很容易的,在 Realm 类的 doGetAuthorizationInfo 方法中 ,把当前用户所属的部门、是否为该部门的数据从数据库查询出来,addStringPermissions 即可。

问题就是鉴权怎么处理,因为Shiro的@RequiresPermissions注解并没有提供这个支持

本文将补全鉴权部分的处理方案。

代码实现

创建 PermissionResolver 接口

其中 resolve() 方法 表示某一资源在权限字符串中的 表示形式,在本例中 即为权限字符串中 ${部门UUID} 的生成方法

public interface PermissionResolver {
    
    

    String resolve();
    
    static List<String> resolve(List<PermissionResolver> list) {
    
    
        return Optional.ofNullable(list).map(obj -> obj.stream().map(PermissionResolver::resolve).collect(toList()))
                .orElse(Collections.emptyList());
    }

}

让需要进行细粒度校验的实体实现 PermissionResolver 接口,本例中为 【部门成员关系】, 本例中所有部门成员均归部门管理员管理,所以 resolve() 直接返回部门uuid即可,如果部门下还分 科室 ,还有一级科室管理员的话 ,则需要返回 ${部门uuid}: ${科室uuid}

public class DepartmentMember implements Serializable, PermissionResolver {
    
    
    
    @Override
    public String resolve() {
    
    
        return departmentUuid;
    }
  
    String uuid;
    String departmentUuid;    
    String userUuid;

定义注解类 RequiresMyPermissions

与原文略有不同 , 因为我们需要构筑出的字符串是前言中的完整字符串,而不是只有一个部门uuid , 而且我们需要区分各种不同的操作(成员都有查询权限,但只有管理员有管理权限),所以注解定义如下

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequiresMyPermissions {
    
    
    String namespace();

    String action();

    /**
     * 前置校验资源权限表达式     *
     */
    String pre() default "";

    /**
     * 后置校验资源权限表达式
     */
    String post() default "";
}

本例中 namespace() = “部门成员” , action() = “邀请” / “移除” / “查询” , pre() 是需要传入的部门UUID

创建认证切面类 PermitAdvisor

代码中需要关注的是构造函数,鉴权过程就在其中。

SpelExpressionParser 为 SpEL表达式解析器 , 它负责把表达式替换为实际的值 (即 占位符替换为部门UUID)

mi.proceed() 表示接口方法的执行,可以看到在它前后分别进行了2次 checkPermission 鉴权 ,其中后一次传入了方法的执行返回值;与原文不同的是这里把注解本体也一并传入,目的是为了拿到 namespace() 和 action() ;

在 checkPermission 方法中我们把 namespace() 、 action() 、 SpEL表达式解析器获取的部门UUID ,拼接得到了完整的权限字符串。并交给Shiro进行校验, SecurityUtils.getSubject().checkPermission 方法将会触发 Realm 类的 doGetAuthorizationInfo 方法请求授权

import com.gin.devicemanagementsystem.sys.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.apache.shiro.SecurityUtils;
import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.List;

@Slf4j
public class PermitAdvisor extends StaticMethodMatcherPointcutAdvisor {
    
    

    private static final Class<? extends Annotation>[] AUTH_ANNOTATION_CLASSES = new Class[]{
    
    RequiresMyPermissions.class};


    public PermitAdvisor() {
    
    
        SpelExpressionParser parser = new SpelExpressionParser();
        // 构造一个通知,当方法上有加入Permitable注解时,会触发此通知执行权限校验
        MethodInterceptor advice = mi -> {
    
    
            Method method = mi.getMethod();
            Object targetObject = mi.getThis();
            Object[] args = mi.getArguments();
            RequiresMyPermissions requiresMyPermissions = method.getAnnotation(RequiresMyPermissions.class);
            // 前置权限认证
            checkPermission(parser, requiresMyPermissions, requiresMyPermissions.pre(), method, args, targetObject, null);
            Object proceed = mi.proceed();
            // 后置权限认证
            checkPermission(parser, requiresMyPermissions, requiresMyPermissions.post(), method, args, targetObject, proceed);
            return proceed;
        };
        setAdvice(advice);
    }


    private void checkPermission(SpelExpressionParser parser, RequiresMyPermissions requiresMyPermissions, String expr, Method method, Object[] args, Object target, Object result) {
    
    
//        表达式为空则通过
        if (StringUtils.isEmpty(expr)) {
    
    
            return;
        }
        Object resources = parser.parseExpression(expr).getValue(createEvaluationContext(method, args, target, result), Object.class);
        final String prefix = requiresMyPermissions.namespace() + ":" + requiresMyPermissions.action() + ":";
        // 调用Shiro进行权限校验
        if (resources instanceof String) {
    
    
            SecurityUtils.getSubject().checkPermission(prefix + resources);
        } else if (resources instanceof List) {
    
    
            List<?> list = (List<?>) resources;
            list.stream().map(obj -> prefix + obj).forEach(SecurityUtils.getSubject()::checkPermission);
        }
    }

    @Override
    public boolean matches(Method method, Class<?> targetClass) {
    
    
        return isAuthAnnotationPresent(method);
    }

    private boolean isAuthAnnotationPresent(Method method) {
    
    
        for (Class<? extends Annotation> annClass : AUTH_ANNOTATION_CLASSES) {
    
    
            Annotation a = AnnotationUtils.findAnnotation(method, annClass);
            if (a != null) {
    
    
                return true;
            }
        }
        return false;
    }


    /**
     * 构造SpEL表达式上下文
     */
    private EvaluationContext createEvaluationContext(Method method, Object[] args, Object target, Object result) {
    
    
        MethodBasedEvaluationContext evaluationContext = new MethodBasedEvaluationContext(
                target, method, args, new DefaultParameterNameDiscoverer());
        evaluationContext.setVariable("result", result);
        try {
    
    
            evaluationContext.registerFunction("resolve", PermissionResolver.class.getMethod("resolve", List.class));
            evaluationContext.setBeanResolver(new BeanFactoryResolver(Initialization.context));

        } catch (NoSuchMethodException e) {
    
    
            log.error("Get method error:", e);
        }
        return evaluationContext;
    }
}

另外 evaluationContext.setBeanResolver 方法会将Spring的BeanFactory注册到上下文中,其中 Initialization.context 为 ApplicationContext 类对象,可以通过实现ApplicationContextAware接口获取。注册后可以在 SpEL表达式 中使用 @+Bean名称 符号引用BeanFactory中的Bean,也即可以使用各种dao,service中的方法

doGetAuthorizationInfo 中进行授权

追加如下代码

查询当前用户所属的部门,若为成员可以查询本部门的成员名单 , 若为部门管理员可以对成员进行邀请和移除

departmentMemberService.listByUserUuid(uuid).forEach(member -> {
    
          
		simpleAuthorizationInfo.addStringPermission(String.format("%s:%s:%s", "部门成员", "查询", member.resolve()));
            if (member.getIsAdmin()) {
    
    
                simpleAuthorizationInfo.addStringPermission(String.format("%s:%s:%s", "部门成员", "移除", member.resolve()));
                simpleAuthorizationInfo.addStringPermission(String.format("%s:%s:%s",  "部门成员", "邀请", member.resolve()));
            }
        });

在接口上用注释标注需要的权限

这里的 pre 中的表达式含义为 当方法参数中的 entity非null时,获取它的 resolve() 作为pre的值 (实际上即为部门uuid) 。更多表达式可以看原文或自行百度

@RequiresMyPermissions(namespace = "部门成员", action = "邀请", pre = "#entity?.resolve()")
    public Res<Void> invite(@RequestBody @Validated DepartmentMember4Create entity) {
    
    
       // 业务逻辑省略了
    }

创建 PermitAdvisor 实例交给Spring容器管理

原文漏了这最后一步,缺少的话整个机制都不会启动

在你的 ShiroConfig.java 类里添加

 @Bean
    public PermitAdvisor permitAdvisor() {
    
    
        return new PermitAdvisor();
    }

猜你喜欢

转载自blog.csdn.net/hjg719/article/details/123576281