




版權說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權,請進行舉報或認領
文檔簡介
1、Java 理論與實踐: 有狀態(tài) Web 應用程序都有漏洞嗎?級別: 中級Brian Goetz, 高級主管工程師, Sun Microsystems2008 年 10 月 13 日Servlets 框架 HttpSession 提供的會話狀態(tài)管理機制簡化了有狀態(tài)應用程序的創(chuàng)建,但也很容易導致誤用。在沒有足夠協(xié)作的情況下,許多 Web 應用程序?qū)勺償?shù)據(jù)(比如 JavaBeans 類)使用了 HttpSession 這個機制,從而使自身面臨大量潛在的并發(fā)性危險。雖然 Java 生態(tài)系統(tǒng)中存在許多 Web 框架,但它們都直接或間接地基于 Servlets 基礎設施。Servlets API 提供大
2、量有用特性,包括通過 HttpSession 和 ServletContext 機制提供的狀態(tài)管理,它允許應用程序在跨多個用戶請求時保持狀態(tài)。然而,在 Web 應用程序中使用共享狀態(tài)受一些微妙的(并且大部分沒有進行說明)的規(guī)則控制著,因此導致許多應用程序無意中觸犯了規(guī)則。結果是許多有狀態(tài) Web 應用程序存在難以發(fā)覺的嚴重缺陷。 范圍容器在 Servlet 規(guī)范中,ServletContext、HttpSession 和 HttpRequest 對象被稱為 范圍容器(Scoped container)。它們都有 getAttribute() 和 setAttribute() 方法,為應用程序存
3、儲數(shù)據(jù)。這些范圍容器的區(qū)別在于它們的生命周期不同。對于 HttpRequest,數(shù)據(jù)只在請求期間有效;對于 HttpSession,數(shù)據(jù)在用戶和應用程序的會話期間有效;而對于 ServletContext,數(shù)據(jù)在應用程序的整個生命周期中都有效。 由于 HTTP 協(xié)議是沒有狀態(tài)的,所以范圍容器在構造有狀態(tài) Web 應用程序時十分有用;servlet 容器負責管理應用程序的狀態(tài)和數(shù)據(jù)的生命周期。盡管這個規(guī)范沒有強調(diào),但需要保證嵌套在會話或應用程序中的容器在某種程度上是線程安全的,因為 getAttribute() 和 setAttribute() 方法隨時都可能被不同的線程調(diào)用(這個規(guī)范沒有直接要
4、求這些實現(xiàn)必須是線程安全的,但它們提供的服務實際上提出了這一點)。范圍容器還為 Web 應用程序提供一個潛在的好處:容器可以透明地管理應用程序狀態(tài)的復制和故障轉(zhuǎn)移。 會話session 是特定用戶和 Web 應用程序之間的一系列請求-響應交換。用戶希望 Web 站點記住他的身份驗證憑證、購物車的物品,以及在前面的請求中輸入到 Web 表單的信息。但核心 HTTP 協(xié)議是沒有狀態(tài)的,這意味著必須將所有請求信息存儲到請求本身。因此,如果要創(chuàng)建比一個請求-響應周期更長的有用交互,就必須存儲會話狀態(tài)。servlet 框架允許將每個請求與一個會話關聯(lián)起來,并提供充當值存儲庫的 HttpSession 接
5、口,用于存儲與該會話相關的(鍵,值)數(shù)據(jù)項。清單 1 是一段典型的 servlet 代碼,它用于在 HttpSession 中存儲購物車數(shù)據(jù):清單 1. 使用 HttpSession 存儲購物車數(shù)據(jù)HttpSession session = request.getSession(true);ShoppingCart cart = (ShoppingCart)session.getAttribute(shoppingCart);if (cart = null) cart = new ShoppingCart(.); session.setAttribute(shoppingCart); doSo
6、methingWith(cart);清單 1 提供的是 servlet 的典型用途;這個應用程序查看是否已將一個對象放到會話中,如果還沒有,它將創(chuàng)建一個該會話的后續(xù)請求可以使用的對象。構建在 servlet 之上的 Web 框架(比如 JSP、JSF 和 SpringMVC 等)隱藏了細節(jié)情況,但對于標記為嵌入到會話中的數(shù)據(jù),它們替您執(zhí)行了實際操作。不幸的是,清單 1 中的用法很可能是不正確的。 與線程相關的問題當 HTTP 請求到達 servlet 容器之后,會在 servlet 容器管理的線程上下文中創(chuàng)建 HttpRequest 和 HttpResponse 對象,并傳遞給 servlet
7、 的 service() 方法。servlet 負責生成響應;在響應完成之前 servlet 一直控制這個線程,響應完成時該線程返回到可用的線程池。Servlet 容器沒有保持線程與會話之間的聯(lián)系;某個會話的下一個請求很可能由另一個不同的線程來處理。事實上,可能有多個請求同時進入同一個會話(當用戶與頁面交互時,使用框架或 AJAX 技術從服務器獲取數(shù)據(jù)的 Web 應用程序可能發(fā)生這種現(xiàn)象)。在這種情況,同一用戶可能發(fā)出多個請求,這些請求在不同的線程上并行執(zhí)行。 大多數(shù)情況下,這類線程問題與 Web 應用程序開發(fā)人員無關。由于自身沒有狀態(tài),HTTP 鼓勵只有存儲在請求中的數(shù)據(jù)(不與其他并發(fā)請求共
8、享)和存儲在存儲庫(比如數(shù)據(jù)庫)中的、已進行并發(fā)性控制的數(shù)據(jù),才具有響應功能。然而,一旦 Web 應用程序?qū)?shù)據(jù)存儲在 HttpSession 或 ServletContext 等共享容器之后,該 Web 應用程序就具有了并發(fā)性,因此必須考慮應用程序內(nèi)部的線程安全問題。 盡管我們常用線程安全描述代碼,但實際上它是描述數(shù)據(jù)的。具體來說,線程安全是指適當?shù)貐f(xié)調(diào)對被多個線程訪問的可變數(shù)據(jù)的訪問。Servlet 應用程序通常是線程安全的,因為它們沒有共享任何可變數(shù)據(jù),因此就不需要額外的同步。但可以通過很多種辦法將共享狀態(tài)引入到 Web 應用程序 除了 HttpSession 和 ServletCont
9、ext 等范圍容器外,還可以使用 HttpServlet 對象的靜態(tài)字段和實例字段。如果要讓 Web 應用程序跨請求共享數(shù)據(jù),開發(fā)人員就必須注意共享數(shù)據(jù)的位置,并保證訪問共享數(shù)據(jù)時線程之間有足夠的協(xié)作(同步),以避免與線程相關的危險。 Web 應用程序的線程風險如果 Web 應用程序?qū)①徫镘嚨瓤勺儠挃?shù)據(jù)存儲在 HttpSession 中,就有可能出現(xiàn)兩個請求試圖同時訪問該購物車。這可能導致以下幾種故障: 原子性故障。在數(shù)據(jù)不一致的狀態(tài)下,一個線程正在更新多個數(shù)據(jù)項,而另一個線程正在讀取這些數(shù)據(jù) 讀線程和寫線程之間的可見性故障。一個線程修改購物車,但另一個線程看到的購物車內(nèi)容的狀態(tài)是過時的或不
10、一致的 原子性故障清單 2 展示了一個用于在游戲應用程序中設置和獲取最高分的不恰當?shù)姆椒▽崿F(xiàn)。它使用一個 PlayerScore 對象來表示最高分,這實際上是一個具有 name 和 score 屬性的普通 JavaBean 類,存儲在內(nèi)嵌于應用程序的 ServletContext 中(假設在應用程序啟動時,初始最高分在 ServletContext 中被設置為 highScore 屬性,因此 getAttribute() 調(diào)用不會失?。?清單 2. 在范圍容器中存儲相關項的不恰當模式public PlayerScore getHighScore() ServletContext ctx =
11、getServletConfig().getServletContext(); PlayerScore hs = (PlayerScore) ctx.getAttribute(highScore); PlayerScore result = new PlayerScore(); result.setName(hs.getName(); result.setScore(hs.getScore(); return result;public void updateHighScore(PlayerScore newScore) ServletContext ctx = getServletConfi
12、g().getServletContext(); PlayerScore hs = (PlayerScore) ctx.getAttribute(highScore); if (newScore.getScore() hs.getScore() hs.setName(newScore.getName(); hs.setScore(newScore.getScore(); 清單 2 中的代碼不夠好。這里采用的方法是在 ServletContext 中存儲一個包含最高分玩家的名字和分數(shù)的可變?nèi)萜?。當打破記錄時,必須更新名字和分數(shù)。 假如當前的最高分玩家是 Bob,他的分數(shù)為 1000,但是 Joe
13、 以 1100 分打破了這個記錄。在正要設置 Joe 的分數(shù)時,另一個玩家又發(fā)出獲得最高分的請求。getHighScore() 將從 servlet 上下文獲取 PlayerScore 對象,然后從該對象獲取名字和分數(shù)。如果不慎出現(xiàn)計時失誤,就有可能獲取 Bob 的名字和 Joe 的分數(shù),顯示 Bob 取得了 1100 分,而實際上 Bob 并沒有獲得這個分數(shù)(這種故障在免費游戲站點上是可以接受的,因為 “分數(shù)” 并不是 “銀行存款”)。這是一種原子性故障,因為彼此之間本應該是原子關系的兩個操作 獲取名字/分數(shù)對和更新名字/分數(shù)對 實際上并沒有按照原子關系執(zhí)行,而且其中一個線程可以在不一致狀態(tài)
14、下查看共享數(shù)據(jù)。另外,由于分數(shù)更新邏輯遵循 check-then-act 模式,因此可能出現(xiàn)兩個線程 “爭奪” 更新最高分,從而導致難以預料的結果。假設當前的最高分是 1000,有兩個玩家同時注冊更高的分數(shù) 1100 和 1200。如果出現(xiàn)計時失誤,這兩個分數(shù)都能夠通過 “高于現(xiàn)有分數(shù)的最高分” 檢查,并且都進入到更新最高分的代碼塊中。和前面一樣,根據(jù)計時的實際情況,最后的結果可能不一致(采用一個玩家的名字和另一個玩家的分數(shù)),或者出現(xiàn)錯誤(分數(shù)為 1100 的玩家可能覆蓋分數(shù)為 1200 的玩家)。 可見性故障 比原子性故障更復雜的是可見性 故障。沒有同步時,如果一個線程讀取另外一個線程正在
15、寫的變量,讀的線程將看到過時的 數(shù)據(jù)。更糟糕的是,讀線程還可能會看到 x 變量的最新數(shù)據(jù)和 y 變量的過時數(shù)據(jù),即使先寫 y 變量也是這樣。可見性故障非常復雜,因為它的發(fā)生是隨機的,甚至是頻繁的,這會導致罕見的難以調(diào)試的間發(fā)性故障。可見性故障是由數(shù)據(jù)爭奪引起的 訪問共享變量時不能正確同步。爭奪數(shù)據(jù)的程序,不管它是什么樣的,都屬于有漏洞的程序,因為它們的行為不能可靠預測。 Java Memory Model(JMM)定義一些條件,它們保證讀變量的線程能夠看到另一個線程的寫入結果(詳細講解 JMM 超出了本文的范圍;參見 參考資料)。JMM 在一個稱為 happens-before 的程序的操作上
16、定義一個排序。只有在通用鎖上執(zhí)行同步或訪問一個通用的可變變量時,才能創(chuàng)建跨線程的 Happens-before 排序。在沒有 happens-before 排序的情況下, Java 平臺可以延遲或更改順序,按照這個順序,一個線程的寫操作對于另一個讀取同一變量的線程是可見的。 清單 2 中的代碼不僅有原子性故障,還有可見性故障。updateHighScore() 方法從 ServletContext 獲取 HighScore 對象,然后修改它的狀態(tài)。這樣做的目的是讓其他調(diào)用 getHighScore() 的線程看見這些修改,但是如果 updateHighScore() 的 name 和 scor
17、e 屬性的寫操作和其他調(diào)用 getHighScore() 的線程的 name 和 score 屬性的讀操作之間沒有 happens-before 排序,我們只能期盼運氣好些,讓讀線程能夠看到正確的值。 可能的解決方案盡管 servlet 規(guī)范沒有充分地描述 servlet 容器必須提供的 happens-before 保證,但可以得出結論:將一個屬性放置在共享范圍容器(HttpSession 或 ServletContext)應該在另一個線程獲取該屬性之前發(fā)生。(參見 JCi 了解這個結論的推理過程。該規(guī)范中這樣描述:“執(zhí)行請求線程的多個 servlets 可能同時積極地訪問單個會話對象。開發(fā)
18、人員負責恰當?shù)赝綄捹Y源的訪問)。 set-after-write 技巧更新存儲在其他會話容器中的可變數(shù)據(jù)時,必須在修改該數(shù)據(jù)后再次調(diào)用 setAttribute()。這是一種常用的最佳實踐。清單 3 展示了重寫 updateHighScore() 以使用這個技巧的示例(這個技巧的目的之一是提示容器值已經(jīng)更改,因此可以在分布式 Web 應用程序的各個實例之間重新同步會話和應用程序狀態(tài))。 清單 3. 使用 set-after-write 技巧提示 servlet 容器值已經(jīng)更新public void updateHighScore(PlayerScore newScore) Servlet
19、Context ctx = getServletConfig().getServletContext(); PlayerScore hs = (PlayerScore) ctx.getAttribute(highScore); if (newScore.getScore() hs.getScore() hs.setName(newScore.getName(); hs.setScore(newScore.getScore(); ctx.setAttribute(highScore, hs); 不幸的是,盡管這個技巧能夠在集群應用程序中高效地復制會話和應用程序狀態(tài),但它不能解決本例中的基本線程安
20、全問題。它可以減輕可見性問題(即另一個玩家可能永遠看不到在 updateHighScore() 中更新的值),但還不能解決許多潛在的原子性問題。 利用同步set-after-write 技巧可以消除可見性問題,因為 happens-before 排序是可傳遞的,因而調(diào)用 updateHighScore() 中的 setAttribute() 和調(diào)用 getHighScore() 中的 getAttribute() 之間有一個邊緣地帶。因為 HighScore 狀態(tài)的更新在 setAttribute() 之前發(fā)生,setAttribute() 狀態(tài)的更新在從 getAttribute() 返回之
21、前發(fā)生,getAttribute() 狀態(tài)的更新在 getHighScore() 的調(diào)用方使用狀態(tài)之前發(fā)生,所以通過這種傳遞可以得出結論:調(diào)用方 getHighScore() 看到的值至少和 setAttribute() 的最近一次調(diào)用一樣新。這個技巧稱為利用同步(piggybacking on synchronization),因為 getHighScore() 和 updateHighScore() 方法能夠在 getAttribute() 和 setAttribute() 中使用同步信息來提供一些可見性保證。然而,在上面這個例子中,這還不能完全解決問題。set-after-write 技
22、巧可能對狀態(tài)復制非常有用,但還不能提供線程安全。 了解不可修改性要創(chuàng)建線程安全的應用程序,一個有用的技巧便是盡可能多地使用不可修改的數(shù)據(jù)。清單 4 展示了重寫后的最高分示例,它使用了 HighScore 的不可修改的 實現(xiàn),從而避免了原子性故障(允許調(diào)用方看見不存在的玩家/分數(shù)對)和可見性故障(阻止 getHighScore() 的調(diào)用方看見在調(diào)用 updateHighScore() 時寫的最新值): 清單 4. 使用不可修改的 HighScore 對象修復原子性和可見性漏洞Public class HighScore public final String name; public fina
23、l int score; public HighScore(String name, int score) = name; this.score = score; public PlayerScore getHighScore() ServletContext ctx = getServletConfig().getServletContext(); return (PlayerScore) ctx.getAttribute(highScore);public void updateHighScore(PlayerScore newScore) ServletContext
24、 ctx = getServletConfig().getServletContext(); PlayerScore hs = (PlayerScore) ctx.getAttribute(highScore); if (newScore.score hs.score) ctx.setAttribute(highScore, newScore); 清單 4 中的代碼的潛在故障很少。在 setAttribute() 和 getAttribute() 中使用同步保證了可見性。實際上,僅存儲單個不可修改數(shù)據(jù)項消除了潛在的原子性故障,即 getHighScore() 的調(diào)用方可以看見名字/分數(shù)對的不一
25、致更新。 將不可修改對象放置在范圍容器避免了許多原子性和可見性故障;將有效不可修改性 對象放置在范圍容器中也是安全的。有效不可修改性對象是指那些雖然理論上是可修改的,但實際上在發(fā)布之后再沒有被更改過的對象,比如 JavaBean,將一個對象放置到 HttpSession 中之后,它的 setter 方法就不再被調(diào)用。 放置在 HttpSession 中的數(shù)據(jù)不僅被該會話的請求訪問;它還可能被容器本身訪問(如果容器進行狀態(tài)復制的話)。所有放置在 HttpSession 或 ServletContext 中的數(shù)據(jù)應該是線程安全的或有效不可修改的。 影響原子狀態(tài)轉(zhuǎn)換但是 清單 4 中的代碼仍然有一個
26、問題 updateHighScore() 中的 check-then-act 仍然使兩個試圖更新最高分數(shù)的線程之間存在潛在 “爭奪”。如果計時失誤,有一個更新可能會丟失。兩個線程可能同時通過了 “高于現(xiàn)有分數(shù)的新最高分” 檢查,造成它們同時調(diào)用 setAttribute()。不能確保兩個分數(shù)中最高者獲得調(diào)用,這取決于計時。要修復這個最后的漏洞,我們需要一種原子性地更新分數(shù)引用的方法,同時又要保證不受干擾。有幾種方法可以實現(xiàn)這個目的。 清單 5 為 updateHighScore() 添加了同步,確保更新進程中固有的 check-then-act 不和另一個更新并發(fā)執(zhí)行。如果所有條件修改邏輯獲得
27、 updateHighScore() 使用的同一個鎖,用這種方法就可以了。 清單 5. 使用同步修復最后一個原子性漏洞public void updateHighScore(PlayerScore newScore) ServletContext ctx = getServletConfig().getServletContext(); PlayerScore hs = (PlayerScore) ctx.getAttribute(highScore); synchronized (lock) if (newScore.score hs.score) ctx.setAttribute(high
28、Score, newScore); 雖然清單 5 中的技術是可行的,但還有一個更好的技術:使用 包中的 AtomicReference 類。這個類的用途就是通過 compareAndSet() 調(diào)用提供原子條件更新。清單 6 展示了如何使用 AtomicReference 來修復本示例的最后一個原子性問題。這個方法比清單 5 中的代碼好,因為很難違背更新最高分數(shù)的規(guī)則。 清單 6. 使用 AtomicReference 來修復最后一個原子性漏洞public PlayerScore getHighScore() ServletContext ctx = getServletConfig().ge
29、tServletContext(); AtomicReference holder = (AtomicReference) ctx.getAttribute(highScore); return holder.get();public void updateHighScore(PlayerScore newScore) ServletContext ctx = getServletConfig().getServletContext(); AtomicReference holder = (AtomicReference) ctx.getAttribute(highScore); while
30、(true) HighScore old = holder.get(); if (old.score = newScore.score) break; else if (pareAndSet(old, newScore) break; 對于放置在范圍容器中的可修改數(shù)據(jù),應該將它們的狀態(tài)轉(zhuǎn)換變成原子性的,這可以通過同步或 中的原子變量類來實現(xiàn)。 序列化對 HttpSession 的訪問在我已給出的示例中,我試圖避免與訪問整個應用程序中的 ServletContext 相關的各種危險。很明顯,訪問 ServletContext 時需要細心的協(xié)作,因為任何請求都可以訪問 ServletContext
31、。然而,大多數(shù)有狀態(tài) Web 應用程序嚴重依賴于內(nèi)嵌于會話的容器 HttpSession。同一個會話中發(fā)生多個同步請求的原因不是很直觀;畢竟,每個會話都是綁定到一個特定用戶或瀏覽器會話的,并且用戶不一定一次請求多個頁面。但是在編程式地生成請求的應用程序中(比如 AJAX 應用程序),一個會話中的請求是可以重疊的。 單個會話中的請求當然可以是重疊的,但這不是一件好事。如果可以輕松地序列化會話中的請求,當訪問 HttpSession 中的共享對象時,這里提到的所有問題幾乎都可以解決;序列化可以阻止原子性故障,并且利用 HttpSession 中的同步可以阻止可見性故障。序列化特定會話的請求不會對吞
32、吐量造成很大的影響,因為一個會話中的請求很少重疊,在一個會話中出現(xiàn)很多請求重疊就更罕見了。 不幸的是,servlet 規(guī)范并沒有提到 “序列化同一會話中的請求”。不過 SpringMVC 框架提供了一種方法,并且這種方法可以在其他框架中輕松實現(xiàn)。SpringMVC 控制器的基類 AbstractController 提供了一個布爾變量 synchronizeOnSession;設置這里之后,它將使用一個鎖,確保一個會話中只同時執(zhí)行一個請求。 序列化 HttpSession 上的請求消除了很多并發(fā)性危險。同樣,將對象限制在 Event Dispatch Thread(EDT)中減少了 Swing 應用程序中的同步需求。結束語許多有狀態(tài) Web 應用程序有很嚴重的并發(fā)
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
- 4. 未經(jīng)權益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責。
- 6. 下載文件中如有侵權或不適當內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 2025-2030建筑工程項目可行性研究報告
- 2025-2030大豆保健食品行業(yè)市場深度調(diào)研及發(fā)展趨勢與投資研究報告
- 2025-2030國際貨貸行業(yè)發(fā)展分析及前景趨勢與投資研究報告
- 2025-2030國內(nèi)瑪瑙飾品行業(yè)市場現(xiàn)狀供需分析及市場深度研究發(fā)展前景及規(guī)劃可行性分析研究報告
- 2025-2030國內(nèi)新生兒黃疸治療儀行業(yè)市場發(fā)展前景及競爭策略與投資風險研究報告
- 2025-2030國內(nèi)人造黃油行業(yè)市場發(fā)展現(xiàn)狀及競爭格局與投資發(fā)展研究報告
- 2025-2030合金鋼粉行業(yè)市場現(xiàn)狀供需分析及重點企業(yè)投資評估規(guī)劃分析研究報告
- 2025-2030十溴二苯乙烷行業(yè)市場現(xiàn)狀供需分析及重點企業(yè)投資評估規(guī)劃分析研究報告
- 2025-2030化妝品玻璃包裝產(chǎn)業(yè)市場深度分析及前景趨勢與投資研究報告
- 2025-2030全球及中國食品工業(yè)真空冷卻設備行業(yè)市場現(xiàn)狀供需分析及市場深度研究發(fā)展前景及規(guī)劃可行性分析研究報告
- 深入理解Zabbix監(jiān)控系統(tǒng)
- 《測繪管理法律與法規(guī)》課件-測繪法律法規(guī)
- 醫(yī)院感染暴發(fā)的應急預案與應急處置演練
- Word操作練習題(解析和答案)
- 駕駛服務外包投標方案(技術標)
- 中國甲狀腺相關眼病診斷和治療指南(2022年)解讀
- 采購電話匯總
- 貴州醫(yī)療服務收費標準-全版
- 創(chuàng)業(yè)管理學習通超星課后章節(jié)答案期末考試題庫2023年
- 興業(yè)銀行零售業(yè)務崗位考試真題模擬匯編(共637題)
- 三年級上冊道德與法治《家是最溫暖的地方》作業(yè)設計
評論
0/150
提交評論