第6章 灵丹妙药 —— OGNL,数据流转的催化剂
6.2 强大的OGNL
OGNL (Object Graph Navigation Language) 是一个开源的表达式引擎。通过使用OGNL,我们能够通过表达式存取Java对象树中的任意属性和调用Java对象树的方法等。也就是说,如果我们把表达式看成是一个带有语义的字符串,那么OGNL就是这个语义字符串与Java对象之间沟通的催化剂,通过OGNL,我们可以轻松解决在数据流转的过程中所碰到的各种问题。
6.2.1深入OGNL的API
我们首先用最直观的方式:通过研究OGNL的原生API来看看如何使用OGNL来进行对象的存取操作。首先来看一下来自于OGNL的静态方法,如代码清单6-4所示:
- /**
- * 通过传入的OGNL表达式,在给定的上下文环境中,从root对象里取值
- */
- public static Object getValue(String expression, Map context, Object root) throws OgnlException {
- return getValue(expression, context, root, null);
- }
- /**
- * 通过传入的OGNL表达式,在给定的上下文环境中,往root对象里写值
- */
- public static void setValue(String expression, Map context, Object root, Object value) throws OgnlException {
- setValue(parseExpression(expression), context, root, value);
- }
OGNL的API其实相当简单,上面的2个方法,分别针对对象的“取值”和“写值”操作。因而,OGNL的基本操作实际上是通过传入上述这2个方法的三个参数来实现的。OGNL同时编写了许多其他的方法来实现相同的功能,上述的2个接口只是其中最简单并最具代表性的2个方法。读者可以通过阅读Ognl.java来获取更多的信息。
在初步浏览了OGNL的API后,我们可以编写一个单元测试来测试一下上面列出来的OGNL静态方法接口,实现如代码清单6-5所示。
- public class BasicOgnlTest extends TestCase {
- @SuppressWarnings("unchecked")
- @Test
- public void testGetValue() throws Exception {
- // 创建Root对象
- User user = new User();
- user.setId(1);
- user.setName("downpour");
- // 创建上下文环境
- Map context = new HashMap();
- context.put("introduction","My name is ");
- // 测试从Root对象中进行表达式计算并获取结果
- Object name = Ognl.getValue(Ognl.parseExpression("name"), user);
- assertEquals("downpour",name);
- // 测试从上下文环境中进行表达式计算并获取结果
- Object contextValue = Ognl.getValue(Ognl.parseExpression("#introduction"), context, user);
- assertEquals("My name is ", contextValue);
- // 测试同时从将Root对象和上下文环境作为表达式的一部分进行计算
- Object hello = Ognl.getValue(Ognl.parseExpression("#introduction + name"), context, user);
- assertEquals("My name is downpour", hello);
- }
- @Test
- public void testSetValue() throws Exception {
- // 创建Root对象
- User user = new User();
- user.setId(1);
- user.setName("downpour");
- // 对Root对象进行写值操作
- Ognl.setValue("group.name", user, "dev");
- Ognl.setValue("age", user, "18");
- assertEquals("dev", user.getGroup().getName());
- }
- }
我们可以看到,通过简单的API就能够完成对各种对象树的“取值”和“写值”操作。而“取值”和“写值”工作是我们日后所有工作的基础,如果我们要深入了解OGNL的细节,就需要对传入OGNL的这3个参数进行研究。这3个参数,被我们称之为OGNL的三要素。在下一节中,我们会对OGNL的三要素做具体的解释。
OGNL的API是极其简单的,无论是何种复杂的功能,OGNL最终会将其最终映射到OGNL的三要素中,通过调用底层引擎完成计算。OGNL对于其构成要素的设计思路,完全契合了我们对表达式引擎的要求,因而也成为了众多表达式引擎设计的一种标准。如果我们翻开其它的一些著名的表达式引擎,同样可以看到这些构成要素的身影。
以Spring框架所发布的内置表达式引擎SpringEL为例,我们可以在其核心的Expression操作接口中看到完全相同的构成要素定义。如图6-2所示:
读者在这里应该仔细品味表达式引擎自身的特性和构成要素之间的联系和共同点,领略其中的设计精髓并熟练运用到实际开发中去。
6.2.2 OGNL三要素
从上一节的例子中我们可以看到,每进行一次OGNL操作都需要3个参数。OGNL的所有操作实际上都是围绕着这3个参数而进行的。这3个参数被称之为OGNL的三要素。
6.2.2.1表达式(Expression)
表达式是整个OGNL的核心,所有的OGNL操作都是针对表达式的解析后进行的。表达式会规定此次OGNL操作到底要干什么。因此,表达式其实是一个带有语法含义的字符串,这个字符串将规定操作的类型和操作的内容。
OGNL支持大量的表达式语法,不仅支持“链式”描述对象访问路径,还支持在表达式中进行简单的计算,甚至还能够支持复杂的Lambda表达式等。我们可以在接下来的章节中看到各种各样不同的OGNL表达式。
6.2.2.2 Root对象(Root Object)
OGNL的Root对象可以理解为OGNL的操作对象。当OGNL表达式规定了“干什么”以后,我们还需要指定对谁干。OGNL的Root对象实际上是一个Java对象,是所有OGNL操作的实际载体。这就意味着,如果我们有一个OGNL的表达式,那么我们实际上需要针对Root对象去进行OGNL表达式的计算并返回结果。
6.2.2.3上下文环境(Context)
有了表达式和Root对象,我们已经可以使用OGNL的基本功能。例如,根据表达式针对OGNL中的Root对象进行“取值”或者“写值”操作。
不过,事实上,在OGNL的内部,所有的操作都会在一个特定的数据环境中运行,这个数据环境就是OGNL的上下文环境(Context)。说得再明白一些,就是这个上下文环境(Context)将规定OGNL的操作在哪里干。
OGNL的上下文环境是一个Map结构,称之为OgnlContext。之前我们所提到的Root对象(Root Object),事实上也会被添加到上下文环境中去,并且将被作为一个特殊的变量进行处理。
6.2.3 OGNL的基本操作
6.2.3.1 对Root对象(Root Object)的访问
针对OGNL的Root对象的对象树的访问是通过使用“点号”将对象的引用串联起来实现的。通过这种方式,OGNL实际上将一个树形的对象结构转化成了一个链式结构的字符串结构来表达语义。
- // 获取Root对象中的name属性的值
- name
- // 获取Root对象中department属性中的name属性的实际值
- department.name
- // 获取Root对象中department属性中manager属性中name属性的实际值
- department.manager.name
6.2.3.2 对上下文环境(Context)的访问
由于OGNL的上下文是一个Map结构,在OGNL进行计算时可以事先在上下文环境中设置一些参数,并让OGNL将这些参数带入进行计算。有时候也需要对这些上下文环境中的参数进行访问,访问这些参数时,需要通过#符号加上链式表达式来进行,从而表示与访问Root对象(Root Object)的区别。
- // 获取OGNL上下文环境中名为introduction的对象的值
- #introduction
- // 获取OGNL上下文环境中名为parameters的对象中user对象中名为name的属性的值
- #parameters.user.name
6.2.3.3 对静态变量的访问
在OGNL中,对于静态变量或者静态方法的访问,需要通过@[class]@[field / method]的表达式语法来进行。
- // 访问com.example.core.Resource 类中名为ENABLE的属性值
- @com.example.core.Resource@ENABLE
- // 调用com.example.core.Resource 类中名为get的方法@com.example.core.Resource@get()
6.2.3.4方法调用
在OGNL中调用方法,可以直接通过类似Java的方法调用方式进行,也就是通过点号加方法名称完成方法调用,甚至可以传递参数。
- // 调用Root对象中的group属性中users的size()方法
- group.users.size()
- // 调用Root对象中的group中的containsUser的方法,并将上下文环境中名为requestUser的值作为参数传入
- group.containsUser(#requestUser)
6.2.3.5使用操作符进行简单计算
OGNL表达式中能使用的操作符基本与Java里的操作符一样,除了能使用 +、 -、 *、/、++、 --、 == 等操作符之外,还能使用 mod、 in、not in等。
- 2+4 // 加
- ‘hello’ + ’‘world‘’ // 字符串叠加
- 5-3 // 减
- 9/2 // 除
- 9 mod 2 // 取模
- foo++ // 递增
- foo == bar // 等于判断
- foo in list // 是否在容器中
6.2.3.6 对数组和容器的访问
OGNL表达式可以支持对数组按照数组下标的顺序进行访问。同样的方法可以用于有序的容器,如ArrayList,LinkedHashSet等。对于Map结构,OGNL支持根据键值进行访问。
- // 访问Root对象中的group属性中users中第一个对象的name属性值
- group.users[0].name
- // 访问OGNL上下文中名为sessionMap的Map对象中key为currentLogonUser的值
- #sessionMap['currentLogonUser']
6.2.3.7投影与选择
OGNL支持类似于数据库中的投影(projection) 和选择(selection)。
投影是指选出集合中每个元素的相同属性组成新的集合,类似于关系数据库的字段操作。投影操作语法为collection.{XXX},其中XXX 是这个集合中每个元素的公共属性。
选择就是过滤满足selection 条件的集合元素,类似于关系数据库的结果集操作。选择操作的语法为:collection.{X YYY},其中X 是一个选择操作符,后面则是选择用的逻辑表达式,而选择操作符有三种:
- ? 选择满足条件的所有元素
- ^ 选择满足条件的第一个元素
- $ 选择满足条件的最后一个元素
- // 返回Root对象中的group属性中users这个集合中所有元素的name构成的集合
- group.users.{name} // 新的以name为元素的集合
- // 将group中users这个集合中的元素的code和name用-连接符拼起来构成的字符串集合
- group.users.{code + ‘-’ + name} // 新的以‘code – name’为元素的集合
- // 返回Root对象的group中users这个集合所有元素中name不为null的元素构成的集合
- group.users.{? #this.name != null} // 过滤后的users集合
6.2.3.8构造对象
OGNL支持直接通过表达式来构造对象。构造的方式主要包括3种:
- 构造List —— 使用{},中间使用逗号隔开元素的方式表达列表
- 构造Map —— 使用#{},中间使用逗号隔开键值对,并使用冒号隔开key和value来构造Map
- 构造对象 —— 直接使用已知对象的构造函数来构造对象
- // 构造一个List
- {"green", "red", "blue"}
- // 构造一个Map
- #{"key1" : "value1", "key2" : "value2", "key3" : "value3"}
- // 构造一个java.net.URL对象
- new java.net.URL("http://localhost/")
构造对象对于一个表达式语言来说是一个非常强大的功能,OGNL不仅能够直接对容器对象构造提供语法层面的支持,还能够对任意的Java对象提供支持。这样一来就使得OGNL不仅仅具备了数据运算这一简单的功能,同时还被赋予了潜在的逻辑计算功能。
【OGNL带来的潜在问题】
我们已经能够看到OGNL在语法层面所表现出来的强大之处。然而,越强大的东西,其自身也一定存在着致命的弱点,这也就是所谓的“物极必反”。正是由于OGNL能够支持完整的Java对象创建、读写过程,它就能被作为一个潜在的切入点,成为黑客的攻击目标。
exploit-db网站在2010年的7月14日就爆出了一个Struts2的远程执行任意代码的漏洞。具体的声明链接为:http://www.exploit-db.com/exploits/14360/。
细心的读者会发现,这个漏洞的基本原理实际上就是利用了OGNL可以任意构造对象,并执行对象中方法的特性,构造了一个底层命令调用的Java类,并执行操作系统命令进行系统攻击。
在Struts2.2.X之后的版本中,这个漏洞被修复,其主要的方法也是通过限制参数名称的方式,拒绝类似的代码执行方式。
6.2.4深入this指针
我们知道,OGNL表达式是以点进行串联的一个链式字符串表达式。而这个表达式在进行计算的时候,从左到右,表达式每一次计算返回的结果成为一个临时的当前对象,并在此临时对象之上继续进行计算,直到得到计算结果。而这个临时的“当前对象”会被存储在一个叫做this的变量中,这个this变量就称之为this指针。
【各种编程语言的this指针】
this指针是许多编程语言都具备的特殊关键字。绝大多数语言中的this指针的含义都是类似的,表示“当前所在函数的调用者”。无论一个表达式有多么复杂,只要读者能够仔细分析this指针所在的函数,并找到这个函数的调用者,就能很容易找到this指针所指向的内容了。
在OGNL的表达式中的this指针,无疑指向了当前计算的“调用者”对应的实例。如果读者从“调用者”这个角度来理解this指针,那么这个概念就能够被消化和理解了。需要注意的是,如果试图在表达式中使用this指针,需要在this之前加上#,我们来看下面的例子。
- // 返回group中users这个集合中所有age比3大的元素构成的集合
- users.{? #this.age > 3}
- // 返回group中users这个集合里的大小+1的值
- group.users.size().(#this+1)
- // 返回Root对象的group中users这个集合所有元素中name不为null的元素构成的集合group.users.{? #this.name != null}
this指针在lambda表达式中运用极为广泛,通过this指针,我们能够写出许多简单而又蕴含着复杂逻辑的OGNL表达式,大家可以在实践中慢慢领悟其中的奥妙。
6.2.5有关#符号的三种用途
在之前的表达式范例中,我们已经了解了“#”操作符的几种不同用途。这是一个非常容易混淆的知识点,所以非常有必要在这里详细解释一下。
- 加在普通OGNL表达式前面,用于访问OGNL上下文中的变量
- 使用#{}语法动态构建Map
- 加在this指针之前表示对this指针的引用
这3种不同的用途在不同的地方有着不同的妙用,尤其是对OGNL上下文中的变量的访问,将成为Struts2在页面级别进行容器变量访问的重要理论基础。