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、小结
从上面的接口测试截图中可以看出,此方案符合我们设计之初要实现的业务场景。
Shiro实现用户对动态资源细粒度的权限校验的更多相关文章
- 用户对动态PHP网页访问过程,以及nginx解析php步骤
www.example.com | Nginx | 路由到www.example.com/index.php | 加载nginx的fast-cgi模块 | fast-cgi监听127.0.0.1:90 ...
- 类Shiro权限校验框架的设计和实现(2)--对复杂权限表达式的支持
前言: 我看了下shiro好像默认不支持复杂表达式的权限校验, 它需要开发者自己去做些功能扩展的工作. 针对这个问题, 同时也会为了弥补上一篇文章提到的支持复杂表示需求, 特地尝试写一下解决方法. 本 ...
- 业务逻辑:五、完成认证用户的动态授权功能 六、完成Shiro整合Ehcache缓存权限数据
一. 完成认证用户的动态授权功能 提示:根据当前认证用户查询数据库,获取其对应的权限,为其授权 操作步骤: 在realm的授权方法中通过使用principals对象获取到当前登录用户 创建一个授权信息 ...
- servlet基本原理(手动创建动态资源+工具开发动态资源)
一.手动开发动态资源 1 静态资源和动态资源的区别 静态资源: 当用户多次访问这个资源,资源的源代码永远不会改变的资源. 动态资源:当用户多次访问这个资源,资源的源代码可能会发送改变. <scr ...
- 手动开发动态资源之servlet初步
1.1 静态资源和动态资源的区别 静态资源:当用户多次访问这个资源,资源的源代码永远不会改变的资源. 动态资源:当用户多次访问这个资源,资源的源代码可能会发送改变. 1.2动态资源的开发技术 Serv ...
- Centos7-yum部署配置LAMP-之LAMP及php-fpm实现反代动态资源
一.简介 LAMP:linux+apache+mysql(这里用mariadb)+php(perl,python) LAMMP:memcached缓存的 CGI:Common Gateway Inte ...
- 静态资源(StaticResource)和动态资源(DynamicResource)
静态资源(StaticResource)和动态资源(DynamicResource) 资源可以作为静态资源或动态资源进行引用.这是通过使用 StaticResource 标记扩展或 DynamicRe ...
- Dynamic Resource – 动态资源
Dynamic Resource – 动态资源 与Static Resource不同的是,Dynamic Resource可以在程序运行时重新评估/计算资源来生成对应的对象/值,它支持向前引用,只 ...
- WPF 资源(StaticResource 静态资源、DynamicResource 动态资源、添加二进制资源、绑定资源树)
原文:WPF 资源(StaticResource 静态资源.DynamicResource 动态资源.添加二进制资源.绑定资源树) 一.WPF对象级(Window对象)资源的定义与查找 实例一: St ...
随机推荐
- 完美解决windows10磁盘占用100%并出现卡顿、假死无反应
完美解决windows10磁盘占用100%并出现卡顿.假死无反应 想必大家也跟我一样,自从用win10系统以后经常会出现这种情况:磁盘突然占用100%然后开始出现假死现象,电脑卡住,点击任何软件没反应 ...
- logback使用配置
一:logback.xml配置内容如下 <?xml version="1.0" encoding="UTF-8"?> <!-- Copyrig ...
- JS 自执行函数
由于自己js基础知识薄弱,很多js的知识还没有掌握,所以接下来会经常写一些关于js基础知识的博客,也算给自己提个醒吧. js自执行函数,听到这个名字,首先会联想到函数.接下来,我来定义一个函数: fu ...
- 【POJ - 2431】Expedition(优先队列)
Expedition 直接中文 Descriptions 一群奶牛抓起一辆卡车,冒险进入丛林深处的探险队.作为相当差的司机,不幸的是,奶牛设法跑过一块岩石并刺破卡车的油箱.卡车现在每运行一个单位的距离 ...
- vue中的虚拟DOM树
什么是虚拟DOM树?(Virtual DOM) 虚拟DOM树其实就是一个普通的js对象,它是用来描述一段HTML片段的 01 当页面渲染的时候Vue会创建一颗虚拟DOM树 02 ...
- 史上最全面的SignalR系列教程-3、SignalR 实现推送功能-集线器类实现方式
1.概述 通过前两篇 史上最全面的SignalR系列教程-1.认识SignalR 史上最全面的SignalR系列教程-2.SignalR 实现推送功能-永久连接类实现方式 文章对SignalR的介绍, ...
- 防止Web攻击,做好HTTP安全标头
前言 下图是几年前一位女性在访谈会上提问Linus(Linux操作系统之父) 为什么英伟达显卡在Linux系统中兼容性这么差? Linus说他们曾经去和英伟达谈过关于显卡在Linux上兼容的问题, ...
- caddy(四)Run详解
caddy(四)Run详解 前言 平时我们使用 caddy 都是使用 它的 二进制 分发文件,我们现在来分析 caddy 的 Run 函数.从最外层抽象的看它都做了些什么. Caddy Run 我们来 ...
- 用小程序·云开发两天搭建mini论坛丨实战
笔者最近涉猎了小程序相关的知识,于是利用周末时间开发了一款类似于同事的小程序,深度体验了小程序云开发模式提供的云函数.数据库.存储三大能力.关于云开发,可参考文档:小程序·云开发. 个人感觉云开发带来 ...
- bootstrap-datetimepicker时间插件使用
html头部引入相关的js和css <link rel="stylesheet" type="text/css" href="css/boots ...