Categories
Uncategorized

Shiro achieve dynamic resource for fine-grained user permissions check

Foreword

In actual system applications, such a common business scenarios, the need to achieve user access to resources to be dynamically permission check.
    For example, in a business system platform, there is a business, brand, product and other business resources. The relationship between them is: a merchant can have multiple brands under one brand can have multiple items.

A business user can have multiple accounts, each account has different levels of authority.
    For example, Wang is responsible for operating the work of all the resources of the business under A, Xiao Zhang is responsible for operating the work of all goods under the brand A and brand A. And Mike is responsible for the brand B

Shiro itself provides RequiresAuthentication, RequiresPermissions and RequiresRoles and other annotations are used to achieve static certification authority,
    But not suitable for dynamic resources such fine-grained permissions certified check. Based on the above description, the article is to add a check for fine-grained access to dynamic resources.

Probably design ideas

    1. Add a custom annotation Permitable, for the conversion of resources into a string representing the rights shiro (support SpEL expression)

    2. Add a new AOP section, used to associate a custom method of tagging and annotation Shiro permissions check

    3. check whether the current user has sufficient privileges to access protected resources

Coding

    1, the new PermissionResolver Interface

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 resolve(List list) {
        return Optional.ofNullable(list).map(obj -> obj.stream().map(PermissionResolver::resolve).collect(toList()))
                .orElse(Collections.emptyList());
    }

}

    2, the new entity class business resources, and implement PermissionResolver interfaces, where the trade resources, for example, such as New 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, the new custom annotation 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, the new authority checking section

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[] 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 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 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, realize authorized users

    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        Map principal = (Map) principals.getPrimaryPrincipal();
        String accountId = (String) principal.get("accountId");

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

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

        return authzInfo;
    }
  • 6, custom annotation application

    6.1, access to business information based on the id

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

    6.2, obtain product information based on the id

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

    6.3, find the list of goods under the brand

      @Permitable(post = "#resolve(#result)")
      @Override
      public List findByBrandId(String brandId) {
          if (StringUtils.isBlank(brandId)) {
              return Collections.emptyList();
          }
          return productDao.findByBrandId(brandId);
      }
  • 7, test

7.1, according to the service scenario described above, the user data to prepare 3

7.2, log on using the Wang test

7.2.1, obtain business information (have permission)

7.2.2, obtain product information (have permission)

7.3, log on using Li test

7.3.1, obtain business information (insufficient privileges)

7.3.2, obtain product information (insufficient privileges)

7.3.3, obtain product information (have permission)

7.4 Summary

As it can be seen from the above screenshot of the interface testing, in line with this program we designed at the beginning of business scenarios to be achieved.

Complete source code

Leave a Reply