本次给大家带来的是另一个很常规但平常却很难想到的一个设计。即在权限控制中引入SpEL来让复杂的权限控制变的更简单,更灵活。
对于在Springboot中,利用自定义注解+切面来实现接口权限的控制这个大家应该都很熟悉,也有大量的博客来介绍整个的实现过程,整体来说思路如下:
-
-
-
-
乍一看,没毛病,学到了,学到了~,收藏起来。但是呢,等到实际用到的时候就傻眼了,为什么呢?在实际的开发中,你会发现,对于权限校验的需求场景是很多的,比如:
-
-
-
-
-
-
-
-
傻眼了不,按照上面的实现逻辑的话怎么搞?加注解?写各种判断?这时候,其实我们就可以通过SpEL表达式来帮我们处理这个问题。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
-
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
-
视频教程:https://doc.iocoder.cn/video/
本文前面提到SpEL,那么到底SpEL是啥呢?
SpEL的全称为Spring Expression Language,即Spring表达式语言。是Spring3.0提供的。他最强大的功能是可以通过运行期间执行的表达式将值装配到我们的属性或构造函数之中。
如果有小伙伴之前没有接触过,不太理解这句话的含义,那么不要紧,继续往下看,通过后续的实践你就能明白他的作用了。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
-
项目地址:https://github.com/YunaiV/yudao-cloud
-
视频教程:https://doc.iocoder.cn/video/
当然,万变不离其宗,自定义注解我们还是需要滴。这里呢,我们仅需要定义一个value属性用于接收表达式即可。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PreAuth {
/**
*
*
* permissionAll()-----只要配置了角色就可以访问
* hasPermission("MENU.QUERY")-----有MENU.QUERY操作权限的角色可以访问
* permitAll()-----放行所有请求
* denyAll()-----只有超级管理员角色才可访问
* hasAuth()-----只有登录后才可访问
* hasTimeAuth(1,,10)-----只有在1-10点间访问
* hasRole(‘管理员’)-----具有管理员角色的人才能访问
* hasAllRole(‘管理员’,'总工程师')-----同时具有管理员、总工程师角色的人才能访问
*
* Spring el
* 文档地址:https://docs.spring.io/spring/docs/5.1.6.RELEASE/spring-framework-reference/core.html#expressions
*/
String value();
}
注解定义好了,我们就需要定义切面了。这里要考虑一个点。我们希望的是如果方法上有注解,则对方法进行限制,若方法上无注解,单是类上有注解,那么类上的权限注解对该类下所有的接口生效。
因此,我们切点的话要用@within注解。代码如下:
@Around(
"@annotation(PreAuth注解路径) || " +
"@within(PreAuth注解路径)"
)
public Object preAuth(ProceedingJoinPoint point) throws Throwable {
if (handleAuth(point)) {
return point.proceed();
}
throw new SecureException(ResultCode.REQ_REJECT);
}
private boolean handleAuth(ProceedingJoinPoint point) {
//TODO 逻辑判断,返回true or false
}
关键点来了。这里我们要引入SpEL。
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
MethodSignature ms = point.getSignature() instanceof MethodSignature? (MethodSignature) point.getSignature():null;
Method method = ms.getMethod();
// 读取权限注解,优先方法上,没有则读取类
PreAuth preAuth = ClassUtil.getAnnotation(method, PreAuth.class);
// 判断表达式
String condition = preAuth.value();
if (StringUtil.isNotBlank(condition)) {
//TODU 表达式解析
}
private boolean handleAuth(ProceedingJoinPoint point) {
MethodSignature ms = point.getSignature() instanceof MethodSignature? (MethodSignature) point.getSignature():null;
Method method = ms.getMethod();
// 读取权限注解,优先方法上,没有则读取类
PreAuth preAuth = ClassUtil.getAnnotation(method, PreAuth.class);
// 判断表达式
String condition = preAuth.value();
if (StringUtil.isNotBlank(condition)) {
Expression expression = EXPRESSION_PARSER.parseExpression(condition);
// 方法参数值
Object[] args = point.getArgs();
StandardEvaluationContext context = getEvaluationContext(method, args);
//获取解析计算的结果
return expression.getValue(context, Boolean.class);
}
return false;
}
/**
* 获取方法上的参数
*
* @param method 方法
* @param args 变量
* @return {SimpleEvaluationContext}
*/
private StandardEvaluationContext getEvaluationContext(Method method, Object[] args) {
// 初始化Sp el表达式上下文,并设置 AuthFun
StandardEvaluationContext context = new StandardEvaluationContext(new AuthFun());
// 设置表达式支持spring bean
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
for (int i = 0; i // 读取方法参数
MethodParameter methodParam = ClassUtil.getMethodParameter(method, i);
// 设置方法 参数名和值 为spel变量
context.setVariable(methodParam.getParameterName(), args[i]);
}
return context;
}
看完上面的解析处理是不是很蒙蔽,只看到了获取表达式,获取参数,设置参数,然后
expression.getValue
就完事了。有的同学会问,你权限校验的逻辑呢?
别急,关键点在这:
StandardEvaluationContext context = new StandardEvaluationContext(new AuthFun());
在上文代码中找到了吧。这个AuthFun就是我们进行权限校验的对象。
所以呢,我们还得在定义一下这个对象。进行具体的权限校验逻辑处理,这里定的每一个方法都可以作为表达式在权限注解中使用。代码如下:
public class AuthFun {
/**
* 判断角色是否具有接口权限
*
* @return {boolean}
*/
public boolean permissionAll() {
//TODO
}
/**
* 判断角色是否具有接口权限
*
* @param permission 权限编号,对应菜单的MENU_CODE
* @return {boolean}
*/
public boolean hasPermission(String permission) {
//TODO
}
/**
* 放行所有请求
*
* @return {boolean}
*/
public boolean permitAll() {
return true;
}
/**
* 只有超管角色才可访问
*
* @return {boolean}
*/
public boolean denyAll() {
return hasRole(RoleConstant.ADMIN);
}
/**
* 是否已授权
*
* @return {boolean}
*/
public boolean hasAuth() {
if(Func.isEmpty(AuthUtil.getUser())){
// TODO 返回异常提醒
}else{
return true;
}
}
/**
* 是否有时间授权
*
* @param start 开始时间
* @param end 结束时间
* @return {boolean}
*/
public boolean hasTimeAuth(Integer start, Integer end)