Spring Security 中,想在权限中使用通配符,怎么做?

Spring Security 中,想在权限中使用通配符,怎么做?插图亿华云

小伙伴们知道,在 Shiro 中,默认是支持权限通配符的,例如系统用户有如下一些权限:

system:user:addsystem:user:deletesystem:user:selectsystem:user:update...

现在给用户授权的时候,我们可以像上面这样,一个权限一个权限的配置,也可以直接用通配符:

system:user:*

这个通配符就表示拥有针对用户的所有权限。

当然这是 Shiro 里边的,对 Shiro 不熟悉的小伙伴,可以在公众号后台回复 shiro,查看松哥之前录的视频教程。

今天我们来聊聊 Spring Security 中对此如何处理,也顺便来看看 TienChin 项目中,这块该如何改进。

1. SpEL

要搞明白基于注解的权限管理,那么得首先理解 SpEL,不需要了解多深入,我这里就简单介绍下。

Spring Expression Language(简称 SpEL)是一个支持查询和操作运行时对象导航图功能的强大的表达式语言。它的语法类似于传统 EL,但提供额外的功能,最出色的就是函数调用和简单字符串的模板函数。

SpEL 给 Spring 社区提供一种简单而高效的表达式语言,一种可贯穿整个 Spring 产品组的语言。这种语言的特性基于 Spring 产品的需求而设计,这是它出现的一大特色。

在我们离不开 Spring 框架的同时,其实我们也已经离不开 SpEL 了,因为它太好用、太强大了,SpEL 在整个 Spring 家族中也处于一个非常重要的位置。但是很多时候,我们对它的只了解一个大概,其实如果你系统的学习过 SpEL,那么上面 Spring Security 那个注解其实很好理解。

我先通过一个简单的例子来和大家捋一捋 SpEL。

为了省事,我就创建一个 Spring Boot 工程来和大家演示,创建的时候不用加任何额外的依赖,就最最基础的依赖即可。

代码如下:

String expressionStr = "1 2";

ExpressionParser parser = new SpelExpressionParser();

Expression exp = parser.parseExpression(expressionStr);

expressionStr 是我们自定义的一个表达式字符串,这个字符串通过一个 ExpressionParser 对象将之解析为一个 Expression,接下来就可以执行这个 exp 了。

执行的时候有两种方式,对于我们上面这种不带任何额外变量的,我们可以直接执行,直接执行的方式如下:

Object value = exp.getValue();

System.out.println(value.toString());

这个打印结果为 3。

我记得之前有个小伙伴在群里问想执行一个字符串表达式,但是不知道怎么办,js 中有 eval 函数很方便,我们 Java 中也有 SpEL,一样也很方便。

不过很多时候,我们要执行的表达式可能比较复杂,这时候上面这种调用方式就不太够用了。

此时我们可以为要调用的表达式设置一个上下文环境,这个时候就会用到 EvaluationContext 或者它的子类,如下:

StandardEvaluationContext context = new StandardEvaluationContext();

System.out.println(exp.getValue(context));

当然上面这个表达式不需要设置上下文环境,我举一个需要设置上下文环境的例子。

例如我现在有一个 User 类,如下:

public class User {

private Integer id;

private String username;

private String address;

//省略 getter/setter

}

现在我的表达式是这样:

String expression = "#user.username";

ExpressionParser parser = new SpelExpressionParser();

Expression exp = parser.parseExpression(expression);

StandardEvaluationContext ctx = new StandardEvaluationContext();

User user = new User();

user.setAddress("广州");

user.setUsername("javaboy");

user.setId(99);

ctx.setVariable("user", user);

String value = exp.getValue(ctx, String.class);

System.out.println("value = " value);

这个表达式就表示获取 user 对象的 username 属性。将来创建一个 user 对象,放到 StandardEvaluationContext 中,并基于此对象执行表达式,就可以打印出来想要的结果。

如果我们将 user 对象设置为 rootObject,那么表达式中就不需要 user 了,如下:

String expression = "username";

ExpressionParser parser = new SpelExpressionParser();

Expression exp = parser.parseExpression(expression);

StandardEvaluationContext ctx = new StandardEvaluationContext();

User user = new User();

user.setAddress("广州");

user.setUsername("javaboy");

user.setId(99);

ctx.setRootObject(user);

String value = exp.getValue(ctx, String.class);

System.out.println("value = " value);

表达式就一个 username 字符串,将来执行的时候,会自动从 user 中找到 username 的值并返回。

当然表达式也可以是方法,例如我在 User 类中添加如下两个方法:

public String sayHello(Integer age) {

return "hello " username ";age=" age;

}

public String sayHello() {

return "hello " username;

}

我们就可以通过表达式调用这两个方法,如下:

(1) 调用有参的 sayHello:

String expression = "sayHello(99)";

ExpressionParser parser = new SpelExpressionParser();

Expression exp = parser.parseExpression(expression);

StandardEvaluationContext ctx = new StandardEvaluationContext();

User user = new User();

user.setAddress("广州");

user.setUsername("javaboy");

user.setId(99);

ctx.setRootObject(user);

String value = exp.getValue(ctx, String.class);

System.out.println("value = " value);

就直接写方法名然后执行就行了。

(2) 调用无参的 sayHello:

String expression = "sayHello";

ExpressionParser parser = new SpelExpressionParser();

Expression exp = parser.parseExpression(expression);

StandardEvaluationContext ctx = new StandardEvaluationContext();

User user = new User();

user.setAddress("广州");

user.setUsername("javaboy");

user.setId(99);

ctx.setRootObject(user);

String value = exp.getValue(ctx, String.class);

System.out.println("value = " value);

这些就都好懂了。

甚至,我们的表达式也可以涉及到 Spring 中的一个 Bean,例如我们向 Spring 中注册如下 Bean:

@Service("us")

public class UserService {

public String sayHello(String name) {

return "hello " name;

}

}

然后通过 SpEL 表达式来调用这个名为 us 的 bean 中的 sayHello 方法,如下:

@Autowired

BeanFactory beanFactory;

@Test

void contextLoads() {

String expression = "@us.sayHello(javaboy)";

ExpressionParser parser = new SpelExpressionParser();

Expression exp = parser.parseExpression(expression);

StandardEvaluationContext ctx = new StandardEvaluationContext();

ctx.setBeanResolver(new BeanFactoryResolver(beanFactory));

String value = exp.getValue(ctx, String.class);

System.out.println("value = " value);

}

给配置的上下文环境设置一个 bean 解析器,这个 bean 解析器会自动跟进名字从 Spring 容器中找打响应的 bean 并执行对应的方法。

当然,关于 SpEL 的玩法还有很多,我就不一一列举了。这里主要是想让小伙伴们知道,有这么个技术,方便大家理解 @PreAuthorize 注解的原理。

总结一下:

在使用 SpEL 的时候,如果表达式直接写的就是方法名,那是因为在构建 SpEL 上下文的时候,已经设置了 RootObject 了,我们所调用的方法,实际上就是 RootObject 对象中的方法。在使用 SpEL 对象的时候,如果像调用非 RootObject 对象中的方法,那么表达式需要加上@对象名​

THE END
Copyright © 2024 亿华云