Shiro实现用户对动态资源细粒度的权限校验

前言

在实际系统应用中,普遍存在这样的一种业务场景,需要实现用户对要访问的资源进行动态权限校验。
譬如,在某平台的商家系统中,存在商家、品牌、商品等业务资源。它们之间的关系为:一个商家可以拥有多个品牌,一个品牌下可以拥有多个商品。

一个商家用户可以拥有多个账户,每个账户拥有不同级别的权限。
例如,小王负责商家A下的所有资源的运营工作,小张负责品牌A和品牌A下所有商品的运营工作。而小李负责品牌B

Shiro本身提供了RequiresAuthentication、RequiresPermissions和RequiresRoles等注解用于实现静态权限认证,
但不适合对于这种细粒度的动态资源的权限认证校验。基于以上描述,这篇文章就是补充了一种对细粒度动态资源的访问权限校验。

大概的设计思路

  • 1.新增一个自定义注解Permitable,用于将资源转换为shiro的权限表示字符串(支持SpEL表达式)
  • 2.新增加一个AOP切面,用于将自定义注解标注的方法和Shiro权限校验关联起来
  • 3.校验当前用户是否拥有足够的权限去访问受保护的资源

编码实现

  • 1、新建PermissionResolver接口
import java.util.Collections;
import java.util.List;
import java.util.Optional;

import static java.util.stream.Collectors.toList;

/**
 * 资源权限解析器
 *
 * @author wuyue
 * @since 1.0, 2019-09-07
 */
public interface PermissionResolver {

    /**
     * 解析资源
     *
     * @return 资源的权限表示字符串
     */
    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());
    }

}
  • 2、新增业务资源实体类,并实现PermissionResolver接口,此处以商品资源为例,例如新建Product.java
import com.wuyue.shiro.shiro.PermissionResolver;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.*;
import java.util.Date;

@Getter
@Setter
@ToString
@Entity
@Table(name = "product")
public class Product implements PermissionResolver {

    @Override
    public String resolve() {
        return merchantId + ":" + brandId + ":" + id;
    }

    @Id
    @GenericGenerator(name = "idGen", strategy = "uuid")
    @GeneratedValue(generator = "idGen")
    private String id;

    @Column(name = "merchant_id")
    private String merchantId;

    @Column(name = "brand_id")
    private String brandId;

    @Column(name = "name")
    private String name;

    @Column(name = "create_time")
    private Date createTime;

    @Column(name = "update_time")
    private Date updateTime;

}
  • 3、新增自定义注解Permitable
import java.lang.annotation.*;

/**
 * 自定义细粒度权限校验注解,配合SpEL表达式使用
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Permitable {

    /**
     * 前置校验资源权限表达式
     *
     * @return 资源的权限字符串表示(如“字节跳动”下的“抖音”可以表达为BYTE_DANCE:TIK_TOK)
     */
    String pre() default "";

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

}
  • 4、新增权限校验切面
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.apache.commons.lang3.StringUtils;
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>[] AUTHZ_ANNOTATION_CLASSES =
            new Class[] {
                    Permitable.class
            };

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

    /**
     * 匹配加了Permitable注解的方法,用于通知权限校验
     */
    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        Method m = method;

        if (isAuthzAnnotationPresent(m)) {
            return true;
        }
        return false;
    }

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

    /**
     * 动态权限认证
     */
    private void checkPermission(SpelExpressionParser parser, String expr,
                                 Method method, Object[] args, Object target, Object result){

        if (StringUtils.isBlank(expr)){
            return;
        }

        // 解析SpEL表达式,获得资源的权限表示字符串
        Object resources = parser.parseExpression(expr)
                .getValue(createEvaluationContext(method, args, target, result), Object.class);

        // 调用Shiro进行权限校验
        if (resources instanceof String) {
            SecurityUtils.getSubject().checkPermission((String) resources);
        } else if (resources instanceof List){
            List<Object> list = (List) resources;
            list.stream().map(obj -> (String) obj).forEach(SecurityUtils.getSubject()::checkPermission);
        }
    }

    /**
     * 构造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));
        } catch (NoSuchMethodException e) {
            log.error("Get method error:", e);
        }
        return evaluationContext;
    }

}
  • 5、实现对用户的授权
    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        Map<String, Object> principal = (Map<String, Object>) principals.getPrimaryPrincipal();
        String accountId = (String) principal.get("accountId");

        // 拥有的商家资源权限
        List<AccountMerchantLink> merchantLinks = accountService.findMerchantLinks(accountId);
        Set<String> merchantPermissions = merchantLinks.stream().map(AccountMerchantLink::getMerchantId).collect(toSet());
        SimpleAuthorizationInfo authzInfo = new SimpleAuthorizationInfo();
        authzInfo.addStringPermissions(merchantPermissions);

        // 拥有的品牌资源权限
        List<AccountBrandLink> brandLinks = accountService.findBrandLinks(accountId);
        Set<String> brandPermissions = brandLinks.stream().map(link -> link.getMerchantId() + ":" + link.getBrandId()).collect(toSet());
        authzInfo.addStringPermissions(brandPermissions);

        return authzInfo;
    }
  • 6、自定义注解的应用

    6.1、根据id获取商家信息

      @Permitable(pre = "#id")
      @Override
      public Optional<Merchant> findById(String id) {
          if (StringUtils.isBlank(id)) {
              return Optional.empty();
          }
          return merchantDao.findById(id);
      }

    6.2、根据id获取商品信息

      @Permitable(post = "#result?.get().resolve()")
      @Override
      public Optional<Product> findById(String id) {
          if (StringUtils.isBlank(id)) {
              return Optional.empty();
          }
          return productDao.findById(id);
      }

    6.3、查找品牌下的商品列表

      @Permitable(post = "#resolve(#result)")
      @Override
      public List<Product> findByBrandId(String brandId) {
          if (StringUtils.isBlank(brandId)) {
              return Collections.emptyList();
          }
          return productDao.findByBrandId(brandId);
      }
  • 7、测试

7.1、按照上面描述的业务场景,准备3个用户数据

7.2、使用小王登录后测试

7.2.1、获取商家信息(拥有权限)

7.2.2、获取商品信息(拥有权限)

7.3、使用小李登录后测试

7.3.1、获取商家信息(权限不足)

7.3.2、获取商品信息(权限不足)

7.3.3、获取商品信息(拥有权限)

7.4、小结

从上面的接口测试截图中可以看出,此方案符合我们设计之初要实现的业务场景。

完整源码

原文地址:https://www.cnblogs.com/felixwu0525/p/11482419.html

时间: 2024-10-27 13:39:31

Shiro实现用户对动态资源细粒度的权限校验的相关文章

类Shiro权限校验框架的设计和实现(2)--对复杂权限表达式的支持

前言: 我看了下shiro好像默认不支持复杂表达式的权限校验, 它需要开发者自己去做些功能扩展的工作. 针对这个问题, 同时也会为了弥补上一篇文章提到的支持复杂表示需求, 特地尝试写一下解决方法. 本文主要借助groovy脚本来实现复杂表达式的计算, 其思想是借鉴了Oval支持复杂表达式(groovy/javascript/ruby)的实现方式. 文章系列: 1. springmvc简单集成shiro  2. 类Shiro权限校验框架的设计和实现  3. 权限系统(RBAC)的数据模型设计 目标

业务逻辑:五、完成认证用户的动态授权功能 六、完成Shiro整合Ehcache缓存权限数据

一. 完成认证用户的动态授权功能 提示:根据当前认证用户查询数据库,获取其对应的权限,为其授权 操作步骤: 在realm的授权方法中通过使用principals对象获取到当前登录用户 创建一个授权信息对象 根据用户查询角色列表,并遍历角色列表 在循环体中将角色关键字添加到授权信息对象的角色属性中 根据用户查询权限列表,并遍历权限列表 在循环体中将权限关键字添加到授权信息对象的权限属性中 在角色与权限service类的根据用户查询角色与权限方法中判断用户是否为系统管理员 如果是系统管理员就查询出所

Java秒杀系统实战系列~整合Shiro实现用户登录认证

摘要: 本篇博文是"Java秒杀系统实战系列文章"的第五篇,在本篇博文中,我们将整合权限认证-授权框架Shiro,实现用户的登陆认证功能,主要用于:要求用户在抢购商品或者秒杀商品时,限制用户进行登陆!并对于特定的url(比如抢购请求对应的url)进行过滤(即当用户访问指定的url时,需要要求用户进行登陆). 内容: 对于Shiro,相信各位小伙伴应该听说过,甚至应该也使用过!简单而言,它是一个很好用的用户身份认证.权限授权框架,可以实现用户登录认证,权限.资源授权.会话管理等功能,在本

springmvc+spring+mybatis+maven项目集成shiro进行用户权限控制

项目结构: 1.maven项目的pom中引入shiro所需的jar包依赖关系 <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.0.1</version> </dependency> <dependency> <groupId>org.ap

静态资源和动态资源

手动开发动态资源 1 静态资源和动态资源的区别 静态资源: 当用户多次访问这个资源,资源的源代码永远不会改变的资源. 动态资源:当用户多次访问这个资源,资源的源代码可能会发送改变. 动态资源的开发技术 Servlet : 用java语言来编写动态资源的开发技术. Servlet特点: 1)普通的java类,继承HttpServlet类,覆盖doGet方法 2)Servlet类只能交给tomcat服务器运行!!!!(开发者自己不能运行!!!) Servlet手动编写步骤: 1)编写一个servle

Centos7-yum部署配置LAMP-之LAMP及php-fpm实现反代动态资源

一.简介 LAMP:linux+apache+mysql(这里用mariadb)+php(perl,python) LAMMP:memcached缓存的 CGI:Common Gateway Interface通用网关接口,说白了就是个简化的httpd协议 httpd+php有三种模式 modules 动态模块模式,最简单最容易配置的方式,httpd启动时会加载模块,加载时将对应的模块激活,php-cgi也就启动了,很多人认为动态编译模块时在需要的时候随时加载调用,不需要的时候就停止,其实动态编

k8s 应用优先级,驱逐,波动,动态资源调整

k8s 应用优先级,驱逐,波动,动态资源调整 应用优先级 Requests 和 Limits 的配置除了表明资源情况和限制资源使用之外,还有一个隐藏的作用:它决定了 Pod 的 QoS 等级. 上一节我们提到了一个细节:如果 Pod 没有配置 Limits ,那么它可以使用节点上任意多的可用资源.这类 Pod 能灵活使用资源,但这也导致它不稳定且危险,对于这类 Pod 我们一定要在它占用过多资源导致节点资源紧张时处理掉.优先处理这类 Pod,而不是处理资源使用处于自己请求范围内的 Pod 是非常

spark动态资源(executor)分配

spark动态资源调整其实也就是说的executor数目支持动态增减,动态增减是根据spark应用的实际负载情况来决定. 开启动态资源调整需要(on yarn情况下) 1.将spark.dynamicAllocation.enabled设置为true.意思就是启动动态资源功能 2.将spark.shuffle.service.enabled设置为true. 在每个nodeManager上设置外部shuffle服务 2.1 将spark-<version>-yarn-shuffle.jar拷贝到

[WPF]静态资源(StaticResource)和动态资源(DynamicResource)

一.文章概述 本演示介绍了WPF的静态资源和动态资源的基本使用,并对两者做了简单的比较. 静态资源(StaticResource)指的是在程序载入内存时对资源的一次性使用,之后就不再访问这个资源了:动态资源(DynamicResource)使用指的是在程序运行过程中然会去访问资源. 相关下载(代码.屏幕录像):http://pan.baidu.com/s/1c0q2BVM 在线播放:http://v.youku.com/v_show/id_XODE4MTI1MzYw.html 温馨提示:如果屏幕