天天看點

《Struts2技術内幕》 新書部分篇章連載(九)—— 強大的OGNL

[b][size=x-large]第6章 靈丹妙藥 —— OGNL,資料流轉的催化劑[/size][/b]

[b][size=large]6.2 強大的OGNL[/size][/b]

OGNL (Object Graph Navigation Language) 是一個開源的表達式引擎。通過使用OGNL,我們能夠通過表達式存取Java對象樹中的任意屬性和調用Java對象樹的方法等。也就是說,如果我們把表達式看成是一個帶有語義的字元串,那麼OGNL就是這個語義字元串與Java對象之間溝通的催化劑,通過OGNL,我們可以輕松解決在資料流轉的過程中所碰到的各種問題。

[b][size=large]6.2.1深入OGNL的API[/size][/b]

我們首先用最直覺的方式:通過研究OGNL的原生API來看看如何使用OGNL來進行對象的存取操作。首先來看一下來自于OGNL的靜态方法,如代碼清單6-4所示:

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所示:

[img]http://dl.iteye.com/upload/attachment/0062/4323/7f0510d4-74d0-3dae-834b-982ac343589e.png[/img]

讀者在這裡應該仔細品味表達式引擎自身的特性和構成要素之間的聯系和共同點,領略其中的設計精髓并熟練運用到實際開發中去。

[b][size=large]6.2.2 OGNL三要素[/size][/b]

從上一節的例子中我們可以看到,每進行一次OGNL操作都需要3個參數。OGNL的所有操作實際上都是圍繞着這3個參數而進行的。這3個參數被稱之為OGNL的三要素。

[b]6.2.2.1表達式(Expression)[/b]

表達式是整個OGNL的核心,所有的OGNL操作都是針對表達式的解析後進行的。表達式會規定此次OGNL操作到底要[b][color=red]幹什麼[/color][/b]。是以,表達式其實是一個帶有文法含義的字元串,這個字元串将規定操作的類型和操作的内容。

OGNL支援大量的表達式文法,不僅支援“鍊式”描述對象通路路徑,還支援在表達式中進行簡單的計算,甚至還能夠支援複雜的Lambda表達式等。我們可以在接下來的章節中看到各種各樣不同的OGNL表達式。

[b]6.2.2.2 Root對象(Root Object)[/b]

OGNL的Root對象可以了解為OGNL的操作對象。當OGNL表達式規定了“幹什麼”以後,我們還需要指定[b][color=red]對誰幹[/color][/b]。OGNL的Root對象實際上是一個Java對象,是所有OGNL操作的實際載體。這就意味着,如果我們有一個OGNL的表達式,那麼我們實際上需要針對Root對象去進行OGNL表達式的計算并傳回結果。

[b]6.2.2.3上下文環境(Context)[/b]

有了表達式和Root對象,我們已經可以使用OGNL的基本功能。例如,根據表達式針對OGNL中的Root對象進行“取值”或者“寫值”操作。

不過,事實上,在OGNL的内部,所有的操作都會在一個特定的資料環境中運作,這個資料環境就是OGNL的上下文環境(Context)。說得再明白一些,就是這個上下文環境(Context)将規定OGNL的操作[b][color=red]在哪裡幹[/color][/b]。

OGNL的上下文環境是一個Map結構,稱之為OgnlContext。之前我們所提到的Root對象(Root Object),事實上也會被添加到上下文環境中去,并且将被作為一個特殊的變量進行處理。

[b][size=large]6.2.3 OGNL的基本操作[/size][/b]

[b]6.2.3.1 對Root對象(Root Object)的通路[/b]

針對OGNL的Root對象的對象樹的通路是通過使用“點号”将對象的引用串聯起來實作的。通過這種方式,OGNL實際上将一個樹形的對象結構轉化成了一個鍊式結構的字元串結構來表達語義。

// 擷取Root對象中的name屬性的值
name     
// 擷取Root對象中department屬性中的name屬性的實際值
department.name   
// 擷取Root對象中department屬性中manager屬性中name屬性的實際值
department.manager.name 
           

[b]6.2.3.2 對上下文環境(Context)的通路[/b]

由于OGNL的上下文是一個Map結構,在OGNL進行計算時可以事先在上下文環境中設定一些參數,并讓OGNL将這些參數帶入進行計算。有時候也需要對這些上下文環境中的參數進行通路,通路這些參數時,需要通過#符号加上鍊式表達式來進行,進而表示與通路Root對象(Root Object)的差別。

[b]6.2.3.3 對靜态變量的通路[/b]

在OGNL中,對于靜态變量或者靜态方法的通路,需要通過@[class]@[field / method]的表達式文法來進行。

[b]6.2.3.4方法調用[/b]

在OGNL中調用方法,可以直接通過類似Java的方法調用方式進行,也就是通過點号加方法名稱完成方法調用,甚至可以傳遞參數。

[b]6.2.3.5使用操作符進行簡單計算[/b]

OGNL表達式中能使用的操作符基本與Java裡的操作符一樣,除了能使用 +、 -、 *、/、++、 --、 == 等操作符之外,還能使用 mod、 in、not in等。

2+4 // 加 
‘hello’ + ’‘world‘’ // 字元串疊加
5-3 // 減
9/2 // 除
9 mod 2 // 取模
foo++ // 遞增
foo == bar // 等于判斷
foo in list // 是否在容器中
           

[b]6.2.3.6 對數組和容器的通路[/b]

OGNL表達式可以支援對數組按照數組下标的順序進行通路。同樣的方法可以用于有序的容器,如ArrayList,LinkedHashSet等。對于Map結構,OGNL支援根據鍵值進行通路。

[b]6.2.3.7投影與選擇[/b]

OGNL支援類似于資料庫中的投影(projection) 和選擇(selection)。

投影是指選出集合中每個元素的相同屬性組成新的集合,類似于關系資料庫的字段操作。投影操作文法為collection.{XXX},其中XXX 是這個集合中每個元素的公共屬性。

選擇就是過濾滿足selection 條件的集合元素,類似于關系資料庫的結果集操作。選擇操作的文法為:collection.{X YYY},其中X 是一個選擇操作符,後面則是選擇用的邏輯表達式,而選擇操作符有三種:

[list]

[*]? 選擇滿足條件的所有元素

[*]^ 選擇滿足條件的第一個元素

[*]$ 選擇滿足條件的最後一個元素

[/list]

[b]6.2.3.8構造對象[/b]

OGNL支援直接通過表達式來構造對象。構造的方式主要包括3種:

[list]

[*]構造List —— 使用{},中間使用逗号隔開元素的方式表達清單

[*]構造Map —— 使用#{},中間使用逗号隔開鍵值對,并使用冒号隔開key和value來構造Map

[*]構造對象 —— 直接使用已知對象的構造函數來構造對象

[/list]

// 構造一個List
{"green", "red", "blue"}
// 構造一個Map
#{"key1" : "value1", "key2" : "value2", "key3" : "value3"}
// 構造一個java.net.URL對象
new java.net.URL("http://localhost/")
           

構造對象對于一個表達式語言來說是一個非常強大的功能,OGNL不僅能夠直接對容器對象構造提供文法層面的支援,還能夠對任意的Java對象提供支援。這樣一來就使得OGNL不僅僅具備了資料運算這一簡單的功能,同時還被賦予了潛在的邏輯計算功能。

【OGNL帶來的潛在問題】

[i]我們已經能夠看到OGNL在文法層面所表現出來的強大之處。然而,越強大的東西,其自身也一定存在着緻命的弱點,這也就是所謂的“物極必反”。正是由于OGNL能夠支援完整的Java對象建立、讀寫過程,它就能被作為一個潛在的切入點,成為黑客的攻擊目标。

exploit-db網站在2010年的7月14日就爆出了一個Struts2的遠端執行任意代碼的漏洞。具體的聲明連結為:http://www.exploit-db.com/exploits/14360/。

細心的讀者會發現,這個漏洞的基本原理實際上就是利用了OGNL可以任意構造對象,并執行對象中方法的特性,構造了一個底層指令調用的Java類,并執行作業系統指令進行系統攻擊。

在Struts2.2.X之後的版本中,這個漏洞被修複,其主要的方法也是通過限制參數名稱的方式,拒絕類似的代碼執行方式。[/i]

[b][size=large]6.2.4深入this指針[/size][/b]

我們知道,OGNL表達式是以點進行串聯的一個鍊式字元串表達式。而這個表達式在進行計算的時候,從左到右,表達式每一次計算傳回的結果成為一個臨時的[b][color=red]目前對象[/color][/b],并在此臨時對象之上繼續進行計算,直到得到計算結果。而這個臨時的“目前對象”會被存儲在一個叫做this的變量中,這個this變量就稱之為this指針。

【各種程式設計語言的this指針】

[i]this指針是許多程式設計語言都具備的特殊關鍵字。絕大多數語言中的this指針的含義都是類似的,表示“目前所在函數的調用者”。無論一個表達式有多麼複雜,隻要讀者能夠仔細分析this指針所在的函數,并找到這個函數的調用者,就能很容易找到this指針所指向的内容了。[/i]

在OGNL的表達式中的this指針,[b][color=red]無疑指向了目前計算的“調用者”對應的執行個體[/color][/b]。如果讀者從“調用者”這個角度來了解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表達式,大家可以在實踐中慢慢領悟其中的奧妙。

[b][size=large]6.2.5有關#符号的三種用途[/size][/b]

在之前的表達式範例中,我們已經了解了“#”操作符的幾種不同用途。這是一個非常容易混淆的知識點,是以非常有必要在這裡詳細解釋一下。

[list]

[*]加在普通OGNL表達式前面,用于通路OGNL上下文中的變量

[*]使用#{}文法動态建構Map

[*]加在this指針之前表示對this指針的引用

[/list]這3種不同的用途在不同的地方有着不同的妙用,尤其是對OGNL上下文中的變量的通路,将成為Struts2在頁面級别進行容器變量通路的重要理論基礎。

繼續閱讀