最近在开发一个安全类的项目,其中涉及到一个功能是根据一些规则判断某些行为是否是恶意的。行为产生的数据实体有固定的的数据结构,需要根据一些规则去校验指定的对象是否满足要求,最开始项目中安全人员是通过falco规则的形式处理,后来发现不够灵活,结果就想到了Spring提供的Spel表达式,试用了一下果然是yyds,全面的匹配符和灵活的规则组装可以覆盖所有场景,下面小编就和大家一起剖析下这个强大的表达式吧。
1、Spel是什么
Spring表达式语言全称为“Spring Expression Language”,缩写为“SpEL”,类似于Struts2x中使用的OGNL表达式语言,能在运行时构建复杂表达式、存取对象图属性、对象方法调用等等,并且能与Spring功能完美整合,如能用来配置Bean定义。Spel表达式不依赖Spring容器,可以单独使用。
2、Spel表达式类型
基本表达式
字面量表达式、关系,逻辑与算数运算表达式、字符串连接及截取表达式、三目运算及Elivis表达式、正则表达式、括号优先级表达式。
字面量表达式:
ExpressionParser parser = new SpelExpressionParser();
// 字符串
String str1 = parser.parseExpression("'test'").getValue(String.class);
String str2 = parser.parseExpression("\"test\"").getValue(String.class);
// 数字类型
int int1 = parser.parseExpression("1").getValue(Integer.class);
long long1 = parser.parseExpression("1L").getValue(long.class);
float float1 = parser.parseExpression("1.1").getValue(Float.class);
double double1 = parser.parseExpression("1.1E+1").getValue(double.class);
int hex1 = parser.parseExpression("0xf").getValue(Integer.class);
// 布尔类型
boolean true1 = parser.parseExpression("true").getValue(boolean.class);
// null类型
Object null1 = parser.parseExpression("null").getValue(Object.class);
算数运算表达式:
SpEL支持:加(+)、减(-)、乘(*)、除(/)、求余(%)、幂(^)等算数运算 ,并且支持用英文替代符号,如:MOD等价%、DIV等价/,且不区分大小写。
ExpressionParser parser = new SpelExpressionParser();
int int1 = parser.parseExpression("3+2").getValue(Integer.class);// 5
int int2 = parser.parseExpression("3*2").getValue(Integer.class);// 6
int int3 = parser.parseExpression("3%2").getValue(Integer.class);// 1
int int4 = parser.parseExpression("3^2").getValue(Integer.class);// 9
关系运算表达式:
SpEL支持:等于(==)、不等于(!=)、大于(>)、大于等于(>=)、小于(<)、小于等于(<=),区间(between)等关系运算。
“EQ” 、“NE”、 “GT”、“GE”、 “LT” 、“LE”来表示等于、不等于、大于、大于等于、小于、小于等于。
并且支持用英文替代符号,如:EQ等价==、NE等价!=、GT等价>、GE等价>=、LT等价<、LE等价<=,且不区分大小写。
ExpressionParser parser = new SpelExpressionParser();
boolean b1 = parser.parseExpression("3==2").getValue(Boolean.class);// false
boolean b2 = parser.parseExpression("3>=2").getValue(Boolean.class);// true
boolean b3 = parser.parseExpression("2 between {2, 3}").getValue(Boolean.class);// true
逻辑运算符表达式:
SpEL支持:或(or)、且(and)、非(!或NOT)。
ExpressionParser parser = new SpelExpressionParser();
boolean b1 = parser.parseExpression("2>1 and false").getValue(Boolean.class);// false
boolean b2 = parser.parseExpression("2>1 or false").getValue(Boolean.class);// true
boolean b3 = parser.parseExpression("NOT false and (2>1 and 3>1)").getValue(Boolean.class);// true
类相关表达式
类类型表达式、类实例化、instanceof表达式、变量定义及引用、赋值表达式、自定义函数、对象属性存取及安全导航表达式、对象方法调用、Bean引用。
访问静态字段:
ExpressionParser parser = new SpelExpressionParser();
double piValue = parser.parseExpression("T(java.lang.Math).PI").getValue(Double.class); // 获取 Math.PI 的值
调用静态方法:
ExpressionParser parser = new SpelExpressionParser();
int randomInt = parser.parseExpression("T(java.lang.Math).random() * 100").getValue(Integer.class); // 获取 0 到 100 之间的随机数
Bean引用:
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("@myBean.someProperty");
String value = expression.getValue(String.class);
属性注入:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class ConfigSeparationExample {
@Value("${app.message}")
private String welcomeMessage;
public void displayWelcomeMessage() {
System.out.println(welcomeMessage);
}
}
变量定义及引用:
SpEL支持:
1、使用#variableName引用通过EvaluationContext接口的
setVariable(variableName, value)方法定义的变量;
2、使用#root引用根对象使用#this引用当前上下文对象
ExpressionParser parser = new SpelExpressionParser();
// #variableName
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("name1", "value1");
String str1 = parser.parseExpression("#name1").getValue(context, String.class);// value1
User user = new User();
user.setId("1");
context = new StandardEvaluationContext(user);
// #root(#root可以省略)
String str2 = parser.parseExpression("#root.id").getValue(context, String.class);// 1
String str3 = parser.parseExpression("id").getValue(context, String.class);// 1
// #this
String str4 = parser.parseExpression("#this.id").getValue(user, String.class);// 1
集合相关表达式
内联List、内联数组、集合,字典访问、列表,字典,数组修改、集合投影、集合选择;不支持多维内联数组初始化;不支持内联字典定义。
遍历集合:
ExpressionParser parser = new SpelExpressionParser();
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> doubledNumbers = parser.parseExpression("{1, 2, 3, 4, 5}.![#this * 2]").getValue(numbers, List.class);
// 结果为 [2, 4, 6, 8, 10]
过滤集合:
ExpressionParser parser = new SpelExpressionParser();
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> filteredNumbers = parser.parseExpression("{1, 2, 3, 4, 5}.?[#this >= 3]").getValue(numbers, List.class);
// 结果为 [3, 4, 5]
模板表达式
模板表达式是一种用于字符串模板和格式化的功能。它允许您在字符串中嵌入 SpEL 表达式,然后通过解析表达式获取实际的值,从而动态地构建字符串。模板表达式可以在配置文件、注解、XML 配置等地方使用,以实现动态文本的生成。
字符串模板和插值:
ExpressionParser parser = new SpelExpressionParser();
String name = "Alice";
String message = parser.parseExpression("'Hello, ${name}!'").getValue(context, String.class); // 结果为 "Hello, Alice!"
3、Spel组件解析
在介绍Spel具体使用之前,我们先来看下Spel表达式由哪几部分组成,又是如何进行工作的。
-
表达式(Expression)
表达式是 SpEL 的核心,它代表了您想要在运行时求值的一段代码。表达式可以由解析器解析,并且使用评估器的上下文进行求值。您可以使用 SpEL 表达式进行各种操作,包括属性访问、方法调用、运算符运算、条件判断等。 -
表达式解析器(ExpressionParser)
表达式解析器是 SpEL 的入口点,它负责创建和解析 SpEL 表达式。通常,您可以使用 SpelExpressionParser 类来创建表达式解析器实例。表达式解析器接受一个表达式字符串作为输入,并返回一个可执行的表达式对象。 -
解析上下文(ParsingContext)
解析上下文包含了解析 SpEL 表达式时的一些配置信息,例如在解析字符串字面值时使用的字符串引号字符、变量前缀、函数前缀等。解析上下文通过表达式解析器来配置,以确保正确地解析表达式字符串。 -
评估上下文(EvaluationContext)
评估器是 SpEL 的核心组件,它在评估 SpEL 表达式时提供了一个执行环境。标准评估器(StandardEvaluationContext)封装了表达式的执行环境,包括变量、函数、类型等。您可以在评估表达式之前,通过评估器设置上下文的属性、方法和其他内容。评估器还支持对 Bean 属性的引用、方法调用和更多操作。
当您使用 SpEL 时,流程大致如下:
-
创建一个表达式解析器实例(通常是 SpelExpressionParser)。
-
创建一个标准评估器上下文(StandardEvaluationContext),您可以在上下文中设置变量、函数、类型等。
-
使用表达式解析器创建一个表达式对象。
-
在评估器上下文中对表达式进行求值,得到结果。
4、使用注意事项
安全性
对于任何表达式的执行我们都需要考虑安全性问题,表达式是动态解析执行的,像Spel这种表达式还可以对类型、函数等进行操作,其实跟我们自己的代码是一样了,如果恶意用户在表达式里面嵌入一个恶意代码随着表达式执行那就麻烦了,所以我们这里需要考虑安全性问题。
EvaluationContext接口有俩个实现类StandardEvaluationContext和SimpleEvaluationContext。SimpleEvaluationContext是一个安全的类,主要的漏洞修复就是靠它来实现的 (但这并不完美,因为我们还需要设置对变量值的过滤)。如果我们不进行设置的话StandardEvaluationContext是SpEL默认使用的EvaluationContext。
SimpleEvaluationContext - 针对不需要SpEL语言语法的全部范围并且应该受到有意限制的表达式类别,公开SpEL语言特性和配置选项的子集。
StandardEvaluationContext - 公开全套SpEL语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。
大家在使用的时候就会发现,使用SimpleEvaluationContext的时候只能支持基本的语法,像一些类型、函数都无法使用。
下面通过一个程序来看看两个对象使用结果的区别:
编写一个表达式启动一个计算器程序:
String inject = "T(java.lang.Runtime).getRuntime().exec('calc.exe')";
StandardEvaluationContext:
StandardEvaluationContext std_c = new StandardEvaluationContext();
SimpleEvaluationContext:
EvaluationContext simple_c = SimpleEvaluationContext.forReadOnlyDataBinding().build();
结果:
第一个结果:exp.getValue(std_c); //计算器将启动
第二个结果:exp.getValue(simple_c); //运行出现错误