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

前言在实际系统应用中,普遍存在这样的一种业务场景,需要实现用户对要访问的资源进行动态权限校验。譬如,在某平台的商家系统中,存在商家、品牌、商品等业务资源。它们之间的关系为:一个商家可以拥有多个品牌,一个品牌下可以拥有多个商品。一个商家用户可以拥有多个账户,每个账户拥有不同级别的权限。例如,小王负责商家A下的所有资源的运营工作,小张负责品牌A和品牌A下所有商品的运营工作。而小李负责品牌BShiro本...

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)@Documentedpublic @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;/** * 静态自定义权限认证切面 */@Slf4jpublic 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 
源文地址:https://www.guoxiongfei.cn/cntech/26530.html