文章目录
参考文章
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();
}