当前位置: 欣欣网 > 码农

SpringBoot中 引入 SpEL,复杂权限控制轻松搞定,非常优雅!

2024-03-04码农

来源:juejin.cn/post/7226674759626571833

本次给大家带来的是另一个很常规但平常却很难想到的一个设计。即在权限控制中引入SpEL来让复杂的权限控制变的更简单,更灵活。

前言

对于在Springboot中,利用自定义注解+切面来实现接口权限的控制这个大家应该都很熟悉,也有大量的博客来介绍整个的实现过程,整体来说思路如下:

  1. 自定义一个权限校验的注解,包含参数value

  2. 配置在对应的接口上

  3. 定义一个切面类,指定切点

  4. 在切入的方法体里写上权限判断的逻辑

乍一看,没毛病,学到了,学到了~,收藏起来。但是呢,等到实际用到的时候就傻眼了,为什么呢?在实际的开发中,你会发现,对于权限校验的需求场景是很多的,比如:

  1. 只要配置了任何角色,就可以访问

  2. 有某个权限就可以访问

  3. 放行所有请求

  4. 只有超级管理员角色才可以访问

  5. 只有登录后才可以访问

  6. 在指定时间段内可以访问

  7. 有某个角色的情况下才可以访问

  8. 同时具有指定的多个角色情况下才可以访问

傻眼了不,按照上面的实现逻辑的话怎么搞?加注解?写各种判断?这时候,其实我们就可以通过SpEL表达式来帮我们处理这个问题。

SpEL表达式

本文前面提到SpEL,那么到底SpEL是啥呢?

SpEL的全称为Spring Expression Language,即Spring表达式语言。是Spring3.0提供的。他最强大的功能是可以通过运行期间执行的表达式将值装配到我们的属性或构造函数之中。

如果有小伙伴之前没有接触过,不太理解这句话的含义,那么不要紧,继续往下看,通过后续的实践你就能明白他的作用了。

开搞

自定义注解

当然,万变不离其宗,自定义注解我们还是需要滴。这里呢,我们仅需要定义一个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。

首先,引入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);
}
returnfalse;
 }
 /**
* 获取方法上的参数
*
* @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 < args.length; 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() {
returntrue;
 }
 /**
* 只有超管角色才可访问
*
* @return {boolean}
*/
 public boolean denyAll() {
return hasRole(RoleConstant.ADMIN);
 }
 /**
* 是否已授权
*
* @return {boolean}
*/
 public boolean hasAuth() {
if(Func.isEmpty(AuthUtil.getUser())){
// TODO 返回异常提醒
}else{
returntrue;
}
 }
 /**
* 是否有时间授权
*
* @param start 开始时间
* @param end 结束时间
* @return {boolean}
*/
 public boolean hasTimeAuth(Integer start, Integer end) {
Integer hour = DateUtil.hour();
return hour >= start && hour <= end;
 }
 /**
* 判断是否有该角色权限
*
* @param role 单角色
* @return {boolean}
*/
 public boolean hasRole(String role) {
return hasAnyRole(role);
 }
 /**
* 判断是否具有所有角色权限
*
* @param role 角色集合
* @return {boolean}
*/
 public boolean hasAllRole(String... role) {
for (String r : role) {
if (!hasRole(r)) {
returnfalse;
}
}
returntrue;
 }
 /**
* 判断是否有该角色权限
*
* @param role 角色集合
* @return {boolean}
*/
 public boolean hasAnyRole(String... role) {
//获取当前登录用户
BladeUser user = AuthUtil.getUser();
if (user == null) {
returnfalse;
}
String userRole = user.getRoleName();
if (StringUtil.isBlank(userRole)) {
returnfalse;
}
String[] roles = Func.toStrArray(userRole);
for (String r : role) {
if (CollectionUtil.contains(roles, r)) {
returntrue;
}
}
returnfalse;
 }
}








实际使用

在使用的时候,我们只需要在类上或者接口上,加上@PreAuth的直接,value值写的时候要注意一下,value应该是我们在AuthFun类中定义的方法和参数,如我们定义了解析方法 hasAllRole(String... role) ,那么在注解中,我们就可以这样写 @PreAuth("hasAllRole('角色1','角色2')") ,需要注意的是,参数要用单引号包括。

@PreAuth("hasPermission('LM_QUERY,LM_QUERY_ALL')")
public T 接口名称....

原理

根据上面的实际使用,可以看到。SpEL表达式解析将我们注解中的" hasAllRole('角色1','角色2') "这样的字符串,给动态解析为了 hasAllRole(参数1,参数1) ,并调用我们注册类中同名的方法。

总结

通过SpEL的使用,让我们的权限配置校验更加灵活。当出现新的场景时,我们仅需要在自定的表达式解析类中增加对应场景的解析方法即可。相对于之前的实现方式,这不得不说是更好的一个选择。

>>

END

精品资料,超赞福利,免费领

微信扫码/长按识别 添加【技术交流群

群内每天分享精品学习资料

最近开发整理了一个用于速刷面试题的小程序;其中收录了上千道常见面试题及答案(包含基础并发JVMMySQLRedisSpringSpringMVCSpringBootSpringCloud消息队列等多个类型),欢迎您的使用。

👇👇

👇点击"阅读原文",获取更多资料(持续更新中