HTTP是一種無連接的協議,如果一個客戶端只是單純地請求一個文件(HTML或GIF),服務器端可以響應給客戶端,並不需要知道一連串的請求是否來自於相同的客戶端,而且也不需要擔心客戶端是否處在連接狀態。但是這樣的通信協議使得服務器端難以判斷所連接的客戶端是否是同一個人。當進行Web程序開發時,我們必須想辦法將相關的請求結合一起,並且努力維持用戶的狀態在服務器上,這就引出了會話追蹤(session tracking)。
1:會話與會話追蹤
session中文經常翻譯為“會話”,其本來的含義是指有始有終的一系列動作或消息,比如打電話時從拿起電話撥號到掛斷電話這中間的一系列過程可以稱之為一個session。有時候可以看到這樣的話“在一個瀏覽器會話期間……”,這里的會話一詞用的就是其本義,是指從一個瀏覽器窗口打開到關閉這個期間;如果說“用戶在一次會話期間……”這樣一句話,它指用戶的一系列動作,比如從登錄到選購商品到結賬登出這樣一個網上購物的過程;然而有時候也可能僅僅是指一次連接。session的含義很多,其中的差別只能靠上下文來推斷。session tracking(會話追蹤)是指一類用來在客戶端與服務器之間保持狀態的解決方案,簡單地說,當一個客戶在多個頁面間切換時,服務器會保存該用戶的信息。
2:實現會話追蹤的4種方式會話追蹤的實現方式有下列4種方式:
(1)使用持續Cookies(Persistent Cookies)。
(2)重寫包含額外參數的URL(URL Rewriting)。
(3)建立含有數據的隱藏表單字段(Hidden Form Field)。
(4)使用內建session對象。
前三種會話追蹤方式是傳統的做法,每種做法都有缺點。最后一種方法是目前最常用,也是最有效的解決方案,因此在這里將把討論重心放在第4種會話追蹤方式上,然而為求徹底了解會話追蹤的機制,還是先將傳統的會話追蹤方式先做一番介紹。(這里和我的理解不太一樣,記錄下我的理解,Session的機制是 Java Servlet 規范規定的,而tomcat container實現了這個規定,tomcat 是通過cookie 和 url rewriting的方式來實現的,也就是通過cookie 或url rewriting 保存一個seesionID, 這樣在內部tomcat 把這個sesionID和一個Map聯系起來,達到將變量和session聯系起來的目的。所以第一和第二中方法是tomcat或其他servlet container實現session機制的手段,當然我們也可以自己實現。而第三種方法只是在兩個頁面跳轉時傳遞變量的一種方式,要想用這種方式實現sesion機制還是不太現實,要每個頁面都寫下hidden數據,而且要寫下所有要傳的變量。)
2.1:使用Cookie
Cookie是一個小小的文本文件,它是將會話信息記錄在這個文本文件內,每個頁面都去Cookie中提取以前的會話信息。例如:
String sessionID = makeUniqueString();
HashMap sessionInfo = new HashMap();
HashMap globalTable = findTableStoringSessions();
globalTable.put(sessionID, sessionInfo);
Cookie sessionCookie =new Cookie("JSESSIONID", sessionID);
sessionCookie.setPath("/");
response.addCookie(sessionCookie);
上面這段代碼先將會話信息記錄在HashMap中,保存在服務器端,並用sessionID標識,然后把sessionID保存在名為“JSESSIONID”的Cookie中。
Cookie[] cookies = request.getCookies();
String sessionid = null;
HashMap sessionInfo = null;
HashMap globalTable = findTableStoringSessions();
if(cookies!=null){
for(int i=0;i<cookies.length;i++){
if(cookies[i].getName().equals("JSESSIONID")){
sessionid = cookies[i].getValue();
break;
}
}
if(sessionid!=null){
sessionInfo = globalTable.get(sessionid);
//We can use the sessionInfo to get value that we want
}
}
用戶請求到達服務器后,先從Cookie中取出sessionID,然后從HashMap中取出會話信息。這樣就實現了會話追蹤。
雖然Cookie強大且持續性高,但是由於有些用戶因為擔心Cookie對個人隱私的威脅,會關閉Cookie,一旦如此,便無法利用Cookie來達到會話追蹤的功能。
2.2:URL重寫
URL重寫是利用GET的方法,在URL的尾部添加一些額外的參數來達到會話追蹤(session tracking)的目的,服務器將這個標識符與它所存儲的有關會話的數據關聯起來。URL看起來如下:
http://host/path/file.html;jsessionid=1234,
使用URL重寫的優點是Cookie被禁用或者根本不支持的情況下依舊能夠工作。但也有很多缺點:
1. 必須對所有指向您的網站的URL進行編碼。
2. 所有頁面必須動態生成。
3. 不能使用預先記錄下來的URL進行訪問,或者從其他網站鏈接進行訪問。
2.3:隱藏表單字段
隱藏表單字段的方法,是利用HTML內hidden的屬性,把客戶端的信息,在用戶不察覺的情形下,偷偷地隨着請求一起傳送給到服務器處理,這樣一來,就可以進行會話跟蹤的任務了。可以下列的方法來做隱藏表單字段的會話追蹤。
<input type="hidden" name="userID" value="15">
然后將重要的用戶信息,如ID之類獨一無二的數據,以隱藏字段的方式傳送給服務器。隱藏字段的優點在於session數據傳送到服務器端時,並不象GET的方法,會將session數據保露在URL之上。不過這種做法還是有它的缺點:一旦session數據儲存在隱藏字段中,就仍然有暴露數據的危機,因為只要用戶直接觀看HTML的源文件,session數據將會暴露無疑。這將造成安全上的漏洞,特別當用戶數據是依賴於用戶ID、密碼來取得的時候,將會有被盜用的危險。另外這種方法只適用特定的一個流程,不適用於通常意義的會話跟蹤。
2.4:使用內建session對象
傳統的會話追蹤方式使用比較麻煩,Servlet的會話機制基於Cookie或URL重寫技術,融合了這兩種技術的優點。當客戶端允許使用Cookie時,內建session對象使用Cookie進行會話追蹤;如果客戶端禁用Cookie,則選擇使用URL重寫。
(1)獲取session對象例如把購物車作為屬性存儲在session中,在其他JSP頁面中可以通過session再獲得購物車。
// 在JSP頁面中可以直接使用session
ShoppingCart cart = (ShoppingCart)session.getAttribute("cart");
內建的session對象是javax.servlet.http.HttpSession類的實例,如果在JavaBean或者Servlet中使用session就需要先從當前的request對象中取得,例如:
// 得到用戶session和購物籃
HttpSession session = request.getSession();
ShoppingCart cart = (ShoppingCart)session.getAttribute("cart");
(2)讀寫session中的數據向session中存入對象使用setAttribute方法,通過getAttribute方法讀取對象。從session返回的值注意要轉換成合適的類型,要注意檢查結 果是否為null。例如下面一段代碼:
HttpSession session = request.getSession();
SomeClass value = (SomeClass)session.getAttribute("someID");
if (value == null) {
value = new SomeClass(...);
session.setAttribute("someID", value);
}
doSomethingWith(value);
(3)廢棄session數據調用removeAttribute廢棄session中的值,即移除與名稱關聯的值。
調用invalidate廢棄整個session,即廢棄當前的session。
如果用戶注銷離開站點,注意廢棄與用戶相關聯的所有session。
(4)session的生命周期由於沒有辦法知道HTTP客戶端是否不再需要session,因此每個session都關聯一個時間期限使它的資源可以被回收。setMaxInactiveInterval(int secondsToLive)
(5)服務器使用session時,默認使用Cookie技術進行會話追蹤,通常,會話管理是通過服務器將 Session ID 作為一個 cookie 存儲在用戶的 Web 瀏覽器中,並用它來唯一標識每個用戶會話。如果客戶端不接受Cookie的時候,服務器可以利用URL重寫的方式將sessionID作為參數附在URL后面,來實現會話管理。
當我們在進行forward,redirect時,一定要調用下邊兩個方法,以保持session一直有效(如果不調用的話,那么你到了新的頁面時session就失效了,因為session ID沒有傳過來)
Servlet中Interface HttpServletResponse 規定了兩個方法,response.encodeURL()或response.encodeRedirectURL()方法,這兩個方法首先判斷Cookies是否被瀏覽器支持;如果支持,則參數URL被原樣返回,session ID將通過Cookies來維持;否則返回帶有sessionID的URL。Tomcat服務器實現了這兩個方法。
下面是使用encodeURL方法的示例,兩個文件hello1.jsp和hello2.jsp。
a: hello1.jsp的完整程序代碼如下:
<%@ page contentType="text/html;charset=gb2312"%>
<%String url =response.encodeURL("hello2.jsp");%>
<a href='<%=url%>'>進入到hello2.jsp</a>
b: 解釋:
hello1.jsp利用了response對象內的encodeURL方法,將URL做了一個編碼動作。編碼不是這里關心的重點,重點是如果瀏覽器的cookie被禁用的話那么象;jsessionid=A09F3A5583825EE787580106CC62A1E8 這樣字符串就會被添加到 hello2.jsp 的后面,也就是告訴了下一個頁面session的信息。
b: 若要使用重定向,例如:
response.sendRedirect("hello2.jsp");也應該改為:response.sendRedirect(response.encodeRedirectURL("hello2.jsp"));同時需要注意的是,將session的ID以URL的編碼方式進行時,需將每一頁都編碼,才能保留住session的ID。如果遇到沒有編碼的URL,則無法進行會話跟蹤。
c: hello2.jsp的完整程序代碼如下:
<%@ page contentType="text/html;charset=gb2312"%>
<% out.println("sessionID is "+session.getId());%>
d: 可以看到如果服務器使用URL重寫,它將會話信息附加到URL上,如下所示:
http://localhost:8080/ch09/hello2.jsp;jsessionid=A09F3A5583825EE787580106CC62A1E8
e: 實質上 URL 重寫是通過向 URL 連接添加參數,並把 session ID 作為值包含在連接中,以便應用服務器可以根據sessionID從cache中的取回session.
TOMCAT服務器
SESSION實現會話跟蹤通常是cookie和url重寫,如果瀏覽器不禁止cookie的話,tomcat優先使用cookie實現,否則它將使用URL重寫來支持SESSION.
URL重寫的額外數據是服務器自動添加的,那么服務器是怎么添加的呢?Tomcat在返回Response的時候,檢查JSP頁面中所有的URL,包括所有的鏈接,和 Form的Action屬性,在這些URL后面加上“;jsessionid=xxxxxx”。添加url后綴的代碼片段如下:
org.apache.coyote.tomcat5.CoyoteResponse類的toEncoded()方法支持URL重寫。
1 StringBuffer sb = new StringBuffer(path);
2 if( sb.length() > 0 ) { // jsessionid can't be first.
3 sb.append(";jsessionid=");
4 sb.append(sessionId);
5 }
6 sb.append(anchor);
7 sb.append(query);
8 return (sb.toString());
服務器端實現原理
Session在服務器端具體是怎么實現的呢?我們使用session的時候一般都是這么使用的:
request.getSession()或者request.getSession(true)。
這個時候,服務器就檢查是不是已經存在對應的Session對象,見HttpRequestBase類
doGetSession(boolean create)方法:
1 if ((session != null) && !session.isValid())
2 session = null;
3 if (session != null)
4 return (session.getSession());
5
6
7 // Return the requested session if it exists and is valid
8 Manager manager = null;
9 if (context != null)
10 manager = context.getManager();
11 if (manager == null)
12 return (null); // Sessions are not supported
13 if (requestedSessionId != null) {
14 try {
15 session = manager.findSession(requestedSessionId);
16 } catch (IOException e) {
17 session = null;
18 }
19 if ((session != null) && !session.isValid())
20 session = null;
21 if (session != null) {
22 return (session.getSession());
23 }
24 }
requestSessionId從哪里來呢?這個肯定是通過Session實現機制的cookie或URL重寫來設置的。見HttpProcessor類中的parseHeaders(SocketInputStream input):
//From COOKIE
1 for (int i = 0; i < cookies.length; i++) {
2 if (cookies[i].getName().equals
3 (Globals.SESSION_COOKIE_NAME)) {//SESSION_COOKIE_NAME="JSESSIONID"???
4 // Override anything requested in the URL
5 if (!request.isRequestedSessionIdFromCookie()) {
6 // Accept only the first session id cookie
7 request.setRequestedSessionId
8 (cookies[i].getValue());
9 request.setRequestedSessionCookie(true);
10 request.setRequestedSessionURL(false);
11
12 }
13 }
14 }
//FROM URL REWRITE
或者HttpProcessor類中的parseRequest(SocketInputStream input, OutputStream output)
1 // Parse any requested session ID out of the request URI
2 int semicolon = uri.indexOf(match); //match 是";jsessionid="字符串
3 if (semicolon >= 0) {
4 String rest = uri.substring(semicolon + match.length());
5 int semicolon2 = rest.indexOf(';');
6 if (semicolon2 >= 0) {
7 request.setRequestedSessionId(rest.substring(0, semicolon2));
8 rest = rest.substring(semicolon2);
9 } else {
10 request.setRequestedSessionId(rest);
11 rest = "";
12 }
13 request.setRequestedSessionURL(true);
14 uri = uri.substring(0, semicolon) + rest;
15 if (debug >= 1)
16 log(" Requested URL session id is " +
17 ((HttpServletRequest) request.getRequest())
18 .getRequestedSessionId());
19 } else {
20 request.setRequestedSessionId(null);
21 request.setRequestedSessionURL(false);
22 }
23
里面的manager.findSession(requestSessionId)用於查找此會話ID對應的session對象。Tomcat實現
是通過一個HashMap實現,見ManagerBase.java的findSession(String id):
1 if (id == null)
2 return (null);
3 synchronized (sessions) {
4 Session session = (Session) sessions.get(id);
5 return (session);
6 }
Session本身也是實現為一個HashMap,因為Session設計為存放key-value鍵值對,Tomcat里面Session實現類是StandardSession,里面一個attributes屬性:
1
4 private HashMap attributes = new HashMap();
所有會話信息的存取都是通過這個屬性來實現的。Session會話信息不會一直在服務器端保存,超過一定的時間期限就會被刪除,這個時間期限可以在web.xml中進行設置,不設置的話會有一個默認值,Tomcat的默認值是60。那么服務器端是怎么判斷會話過期的呢?原理服務器會啟動一個線程,一直查詢所有的Session對象,檢查不活動的時間是否超過設定值,如果超過就將其刪除。見StandardManager類,它實現了Runnable接口,里面的run方法如下:
1
4 public void run() {
5
6 // Loop until the termination semaphore is set
7 while (!threadDone) {
8 threadSleep();
9 processExpires();
10 }
11
12 }
13
14
17 private void processExpires() {
18
19 long timeNow = System.currentTimeMillis();
20 Session sessions[] = findSessions();
21
22 for (int i = 0; i < sessions.length; i++) {
23 StandardSession session = (StandardSession) sessions[i];
24 if (!session.isValid())
25 continue;
26 int maxInactiveInterval = session.getMaxInactiveInterval();
27 if (maxInactiveInterval < 0)
28 continue;
29 int timeIdle = // Truncate, do not round up
30 (int) ((timeNow - session.getLastUsedTime()) / 1000L);
31 if (timeIdle >= maxInactiveInterval) {
32 try {
33 expiredSessions++;
34 session.expire();
35 } catch (Throwable t) {
36 log(sm.getString("standardManager.expireException"), t);
37 }
38 }
39 }
40
41 }