深入理解和改進JSP/Servlet會話管理機制

來源:互聯網
上載者:User

在Web伺服器端編程中,工作階段狀態管理是一個經常必須考慮的重要問題。本文分析JSP/Servlet的會話管理機制及其所面臨的問題,然後提出了一種改進的會話管理方法。

一、Servlet的會話管理機制

根據設計,HTTP是一種無狀態的協議。它意味著Web應用並不瞭解有關同一使用者以前請求的資訊。維持工作階段狀態資訊的方法之一是使用Servlet或者JSP容器提供的會話跟蹤功能。Servlet API規範定義了一個簡單的HttpSession介面,通過它我們可以方便地實現會話跟蹤。

HttpSession介面提供了儲存和返回標準會話屬性的方法。標準會話屬性如工作階段識別項、應用資料等,都以“名字-值”對的形式儲存。簡而言之,HttpSession介面提供了一種把對象儲存到記憶體、在同一使用者的後繼請求中提取這些對象的標準辦法。在會話中儲存資料的方法是setAttribute(String s, Object o),從會話提取原來所儲存對象的方法是getAttribute(String s)。

在HTTP協議中,當使用者不再活動時不存在顯式的終止訊號。由於這個原因,我們不知道使用者是否還要再次返回,如果不採取某種方法解決這個問題,記憶體中會積累起大量的HttpSession對象。

為此,Servlet採用“逾時限制”的辦法來判斷使用者是否還在訪問:如果某個使用者在一定的時間之內沒有發出後繼請求,則該使用者的會話被作廢,他的HttpSession對象被釋放。會話的預設逾時間隔由Servlet容器定義。這個值可以通過getMaxInactiveInterval方法獲得,通過setMaxInactiveInterval方法修改,這些方法中的逾時時間以秒計。如果會話的逾時時間值設定成-1,則會話永不逾時。Servlet可以通過getLastAccessedTime方法獲得當前請求之前的最後一次訪問時間。

要獲得HttpSession對象,我們可以調用HttpServletRequest對象的getSession方法。為了正確地維持工作階段狀態,我們必須在發送任何應答內容之前調用getSession方法。
使用者會話既可以用手工方法作廢,也可以自動作廢。作廢會話意味著從記憶體中刪除HttpSession對象以及它的資料。例如,如果一定時間之內(預設30分鐘)使用者不再發送請求,Java Web Server自動地作廢他的會話。

Servlet/JSP會話跟蹤機制有著一定的局限,比如:

· 會話對象儲存在記憶體之中,佔用了可觀的資源。

· 會話跟蹤依賴於Cookie。由於各種原因,特別是安全上的原因,一些使用者關閉了Cookie。

· 會話跟蹤要用到伺服器建立的工作階段識別項。在多個Web伺服器以及多個JVM的環境中,Web伺服器不能識別其他伺服器建立的工作階段識別項,會話跟蹤機制無法發揮作用。
要深入理解會話跟蹤機制,首先我們必須理解在Servlet/JSP容器中會話如何運作。

二、工作階段識別項

每當新使用者請求一個使用了HttpSession對象的JSP頁面,JSP容器除了發回應答頁面之外,它還要向瀏覽器發送一個特殊的數字。這個特殊的數字稱為“工作階段識別項”,它是一個唯一的使用者識別碼。此後,HttpSession對象就駐留在記憶體之中,等待同一使用者返回時再次調用它的方法。

在用戶端,瀏覽器儲存工作階段識別項,並在每一個後繼請求中把這個工作階段識別項發送給伺服器。工作階段識別項告訴JSP容器當前請求不是使用者發出的第一個請求,伺服器以前已經為該使用者建立了HttpSession對象。此時,JSP容器不再為使用者建立新的HttpSession對象,而是尋找具有相同工作階段識別項的HttpSession對象,然後建立該HttpSession對象和當前請求的關聯。

工作階段識別項以Cookie的形式在伺服器和瀏覽器之間傳送。如果瀏覽器不支援Cookie又如何呢?此時,對伺服器的後繼請求將不會帶有工作階段識別項。結果,JSP容器認為該請求來自一個新使用者,它會再建立一個HttpSession對象,而以前建立的HttpSession對象仍舊駐留在記憶體中,但該使用者以前的會話資訊卻丟失了。

另外,Servlet/JSP容器只認可它自己建立的工作階段識別項。如果同一Web應用在“Web農場”(Web farm)的多台伺服器上運行,則必須存在這樣一種機制:保證來自同一使用者的請求總是被定向到處理該使用者第一次請求的伺服器。

三、偽會話管理機制

如前所述,基於Cookie的會話管理技術面臨著種種問題。下面我們要設計一種新的會話管理機制來解決這些問題。這種會話管理機制稱為“偽會話”(Pseudo Session)機制,它具有如下特點:

· 對象和資料不是儲存在記憶體中,而是以文字檔形式儲存。每一個文字檔與一個特定的使用者關聯,檔案的名字就是會話的標識符。因此,檔案名稱字必須是唯一的。
· 文字檔儲存在一個專用的目錄中,所有Web伺服器都可以訪問這個目錄。因此,偽會話可以用於Web農場。
· 工作階段識別項不作為Cookie發送,而是直接編碼到URL裡面。因此,採用偽會話技術要求修改所有的超級連結,包括HTML表單的ACTION屬性。
此外,實現偽會話管理機制時我們還要考慮到以下幾點:
· 它應該與應用無關,其他想要實現同樣功能的開發人員應該能夠方便地重用它。
· 考慮到安全原因,應該有一種為工作階段識別項產生隨機數位辦法。
· 為了作廢到期的會話,應該設定一個逾時值。同一個使用者,如果他超過一定的時間之後再次返回,他將獲得一個新的工作階段識別項。此舉能夠防止未經授權的使用者冒用其他人的會話。
· 應該有一種收集到期會話並刪除相應文字檔的機制。
· 如果使用者使用已經到期的工作階段識別項再次訪問伺服器,即使這個工作階段識別項的文字檔還沒有刪除,系統也不應該允許使用者使用原來的會話。
· 同時,應該存在一種更新會話文字檔最後改動時間的機制,使得使用者在會話到期時限之前返回時會話總是保持最新且合法的狀態資料。

四、實現偽會話管理機制

下面所介紹的工程稱為PseudoSession,它是偽會話機制一個很簡單的實現。考慮到移植性,我們以JavaBean的形式實現它。PseudoSessionBean的完整代碼可以從本文後面下載。

PseudoSessionBean擁有如下域(Field):
public String path;public long timeOut;
path是儲存所有會話文字檔的目錄。如果Web伺服器的數量在一個以上,這個目錄必須允許所有伺服器訪問。然而,為了防止使用者直接存取這些文字檔,這個路徑應該不允許使用者直接存取。解決這個問題的一種方法是使用Web網站根之外的目錄。

timeOut是使用者的最後一個請求到會話到期作廢之間的時間。在PseudoSessionBean的代碼清單中,timeOut設定成了以毫秒錶示的20分鐘,這是一個比較合理的逾時時間值。對於任何使用者,如果他在這個逾時時間之後才繼續發出請求,他將得到一個新的工作階段識別項。
PseudoSessionBean有4個方法:getSessionID,setValue,getValue,deleteAllInvalidSessions。

4.1 getSessionID方法
getSessionID方法的聲明如下:
public String getSessionID(HttpServletRequest request)
這個方法應該在每一個JSP頁面的開頭調用。它完成如下任務:
· 如果使用者是第一次訪問,則為該使用者設定一個新的工作階段識別項。
· 檢查URL所帶工作階段識別項的合法性。如果工作階段識別項已經到期,則getSessionID方法返回一個新的工作階段識別項。
下面我們來看看getSessionID方法的工作過程。
String sessionId = request.getParameter("sessionId");
validSessionIdFound是一個標記,用於指示工作階段識別項是否合法。validSessionIdFound的初始值是false。
boolean validSessionIdFound = false;
long類型的now變數包含請求出現時的伺服器時間。該變數用於確定使用者會話的合法性。
long now = System.currentTimeMillis();
如果找到了工作階段識別項,則getSessionID方法檢查它的合法性。檢查過程如下:
· 一個合法的工作階段識別項必須有對應的同名文字檔。
· 檔案的最後修改時間加上timeOut應該大於目前時間。
· 如果存在與會話對應的文字檔,但檔案已經到期,則原來的檔案被刪除。
· 把合法工作階段識別項所對應文字檔的最後修改日期改為now。
這些任務主要藉助File對象完成,建立File對象的參數就是會話文字檔的路徑:
if (sessionId!=null) {File f = new File(path + sessionId);if (f.exists()) { if (f.lastModified() + timeOut > now) { // 會話合法// 使用setLastModified時,如果檔案已經被其他程式鎖定,// 程式不會產生任何異常,但檔案資料不會改變f.setLastModified(now);validSessionIdFound = true; } else { // 會話已經到期 // 刪除檔案f.delete(); }} // end if (f.exists) } // end if (sessionId!=null)
如果不存在合法的工作階段識別項,則getSessionID方法產生一個工作階段識別項以及相應的文字檔:
if (!validSessionIdFound) { sessionId = Long.toString(now); // 建立檔案 File f = new File(path + sessionId); try {f.createNewFile(); } catch (IOException ioe) {}} // end of if !validSessionIdFound
程式保證檔案名稱字隨機性的方法非常簡單:把當前的系統時間直接轉換成工作階段識別項。對於那些涉及敏感性資料的應用,我們應該考慮運用更安全的隨機數產生器來產生工作階段識別項。
綜上所述,getSessionID並不總是返回新的合法工作階段識別項:它返回的標識符可能與傳遞給它的標識符相同,也可能是新建立的工作階段識別項。
為了保證JSP頁面擁有合法的工作階段識別項以便調用setValue、getValue方法,每個JSP頁面都必須在開頭位置調用getSesstionID方法。

4.2 setValue方法
setValue方法儲存value字串以及與它關聯的字串名字。這種“名字-值”對很容易使人想起Dictionary對象。setValue方法要求在第一個參數中提供合法的工作階段識別項,它假定在自己被調用之前getSessionID方法已經執行,經過檢驗的合法工作階段識別項必然存在,因此它不再對傳入的工作階段識別項進行合法性檢驗。
setValue方法按如下規則儲存名字-值對:
· 如果與value值關聯的name以前還沒有儲存過,則新的名字-值對加入到文字檔的末尾。
· 如果value字串關聯的name值以前已經儲存過,則原來儲存的值被新的value值替換。
setValue方法按照如下格式儲存名字-值對,注意“名字”是大小寫敏感的:
name-1 value-1name-2 value-2name-3 value-3...name-n value-n
setValue方法的聲明如下:
public void setValue(String sessionId, String name, String value)
setValue方法首先尋找與當前會話對應的文字檔。如果不能找到文字檔,則setValue方法不做任何事情直接返回。如果找到了會話文字檔,setValue方法讀取文字檔的各個行,然後比較讀入的行與name:如果讀入的文本行開頭與name一樣,則說明該名字已經儲存,setValue方法將替換該行後面的值;如果name不能與讀入的文本行匹配,則這行文本被直接複製到一個臨時檔案。
這部分功能的實現代碼如下:
try { FileReader fr = new FileReader(path + sessionId); BufferedReader br = new BufferedReader(fr); FileWriter fw = new FileWriter(path + sessionId + ".tmp"); BufferedWriter bw = new BufferedWriter(fw); String s; while ((s = br.readLine()) != null)if (!s.startsWith(name + " ")) { bw.write(s); bw.newLine();} bw.write(name + " " + value); bw.newLine(); bw.close(); br.close(); fw.close(); bw.close(); . . .}catch (FileNotFoundException e) {}catch (IOException e) { System.out.println(e.toString());}
原來文字檔中的所有行複製到臨時檔案之後,setValue方法刪除原來的文字檔,然後把臨時檔案改成會話文字檔的名字:
File f = new File(path + sessionId + ".tmp");File dest = new File(path + sessionId);dest.delete();f.renameTo(dest);

4.3 getValue方法
getValue方法用於提取原來儲存在偽會話中的資料。正如setValue方法,getValue方法也要求傳入一個合法的工作階段識別項,而且getValue方法不再對傳入的工作階段識別項進行合法性檢查。getValue方法的第二個參數是待提取資料的name,傳回值是與指定name關聯的value。
getValue方法的聲明如下:
public String getValue(String sessionId, String name)
getValue方法的基本執行過程如下:首先找到會話文字檔,然後按行讀入直至找到與name匹配的文本行;找到匹配的文本行之後,getValue方法返回該行儲存的value;如果不能找到,getValue方法返回null。

4.4 deleteAllInvalidSessions方法
deleteAllInvalidSessions方法刪除那些與已經到期的會話關聯的文字檔。由於調用getSessionID方法時到期的會話文字檔會被刪除,deleteAllInvalidSessions方法並不是關鍵的方法。什麼時候調用這個方法由應用自己決定。例如,我們可以編寫一個專用的背景程式,由這個程式每天一次清除所有到期的文字檔。最簡單的辦法是在JSP檔案末尾調用deleteAllInvalidSessions方法,但如果網站比較繁忙,重複地調用deleteAllInvalidSessions方法將降低整個網站的響應能力。一種明智的做法是:編寫一個在訪問量較少的時候自動進行清理的背景程式。
deleteAllInvalidSessions方法的聲明如下:
public void deleteAllInvalidSessions()
它首先把所有會話文字檔的名字讀入files字串數組:
File dir = new File(path); String[] files = dir.list();
deleteAllInvalidSessions方法比較文字檔的最後修改時間(加上逾時時間)和系統目前時間,確定會話是否到期。long類型的變數now用於儲存系統的目前時間。
long now = System.currentTimeMillis();
接下來,deleteAllInvalidSessions方法通過逐一查看files數組,依次檢查每個檔案的lastModified屬性。所有與到期會話關聯的檔案都將被刪除:
for (int i=0; i<files.length; i++) { File f = new File(path + files[i]); if (f.lastModified() + timeOut > now) f.delete(); // 刪除到期的會話文字檔}

五、應用執行個體

編譯好PseudoSessionBean這個JavaBean之後,我們就可以利用偽會話管理機制來管理Web應用的工作階段狀態資訊了。由於不必再使用伺服器的會話管理機制,我們可以在page指令中把session屬性設定為false關閉預設的JSP/Servlet會話管理功能。
<%@ page session="false" %>

然後,我們用JSP的<jsp:useBean>標記告訴JSP容器程式要使用PseudoSessionBean:
<jsp:useBean id="PseudoSessionId" scope="application" class="pseudosession.PseudoSessionBean" />

在上面這個<jsp:useBean>標記中,class屬性值是“包.類名字”形式。當然,對於不同的包名字,class屬性的值應該作相應的修改。注意Bean的scope屬性是“application”,這是因為我們要在應用的所有頁面中使用這個Bean。在這個應用中,把Bean的scope屬性設定為“application”具有最好的效率,因為我們只需建立Bean對象一次就可以了。另外,正如前面所提到的,getSessionID方法必須在所有其他代碼之前調用。
<% String sessionId = PseudoSessionId.getSessionID(request);%>
為了說明PseudoSessionBean的應用,下面我們來看兩個JSP頁面,它們是index.jsp和secondPage.jsp。index.jsp頁面在偽會話變數中儲存使用者的名字,而secondPage.jsp則提取這個使用者名稱字。

index.jsp頁面的代碼如下:
<%@ page session="false" contentType="text/html;charset=gb2312" %>
<jsp:useBean id="PseudoSessionId" scope="application" class="pseudosession.PseudoSessionBean" />
<% String sessionId = PseudoSessionId.getSessionID(request);%>
<html>
<head>
<title>偽會話</title>
</head>
<body>
<h1>偽會話管理機制</h1>
<% String userName = "bulbul"; PseudoSessionId.setValue(sessionId, "userName", userName);%>
<a href=secondPage.jsp?sessionId=<%=sessionId%>>點擊此處</a>
<form method="post" action=anotherPage.jsp?sessionId=<%=sessionId%>>
輸入資料:<input type="text" name="sample">
<input type="submit" name="Submit" value="Submit">
</form>
</body>
</html>
<% PseudoSessionId.deleteAllInvalidSessions();%>

注意,包括<form>標記的action屬性在內,所有的超級連結都已經改寫,現在都包含了工作階段識別項。另外也請注意頁面的最後調用了deleteAllInvalidSessions方法。
secondPage.jsp頁面只簡單地返回以前儲存的使用者名稱字。
<%@ contentType="text/html;charset=gb2312" page session="false" %>
<jsp:useBean id="PseudoSessionId" scope="application" class="pseudosession.PseudoSessionBean" />
<% String sessionId = PseudoSessionId.getSessionID(request);%>
<html>
<head>
<title>第2個頁面</title>
</head>
<body>
<% String userName = PseudoSessionId.getValue(sessionId, "userName"); out.println("使用者名稱字是 " + userName);%>
</body>
</html>

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.