Tomcat之Session和Cookie大揭密
一、JSP和Servlet中的Cookie
由于HTTP協議是無狀態協議(雖然Socket連接是有狀態的,但每次用HTTP協議進行數據傳輸后就關閉的Socket連接,因此,HTTP協議并不會保存上一次的狀態),因此,如果要保存某些HTTP請求過程中所產生的數據,就必須要有一種類似全局變量的機制保證數據在不同的HTTP請求之間共享。這就是下面要講的Session和Cookie。
Cookie是通過將數據保存在客戶端的硬盤(永久Cookie)或內存(臨時Cookie)中來實現數據共享的一種機制。在Windows下,保存在這些Cookie數據的目錄一般是C:\Documents and Settings\Administrator\Cookies。每一個Cookie有一個超時時間,如果超過了這個時間,Cookie將自動失效。可按如下方法來設置Cookie的超時時間:
Cookie cookie = new Cookie("key","value");
cookie.setMaxAge(3600); // Cookie的超時間為3600秒,也就是1小時
response.addCookie(cookie);
如果不使用setMaxAge方法,Cookie的超時時間為-1,在這種情況下,Cookie就是臨時Cookie,也就是說這種Cookie實際上并不保存在客戶端硬盤上,而是保存在客戶端內存中的。讀者可以在JSP中運行如下代碼,看看是否會在上面提到的保存cookie的目錄中生成cookie文件:
Cookie cookie = new Cookie("key","value");
response.addCookie(cookie);
實際上使用setMaxAge將超時時間設為任意的負數都會被客戶端瀏覽器認為是臨時
Cookie,如下面的代碼將在客戶端內存中保存一個臨時Cookie:
Cookie cookie = new Cookie("key","value");
cookie.setMaxAge(-100); // 將cookie設為臨時Cookie
response.addCookie(cookie);
如果第一次將Cookie寫入客戶端(不管是硬盤還是內存),在同一臺機器上第二次訪問
該網站的jsp頁面時,會自動將客戶端的cookie作為HTTP請求頭的Cookie字段值傳給服務端,如果有多個Cookie,中間用";"隔開。如下面的HTTP請求頭所示:
GET /test/First.jsp HTTP/1.1
HOST:localhost
...
Cookie:key1=value1;key2=value2
...
...
我們可以在JSP中使用如下的Java代碼來輸出Cookie字段的值:
out.println(request.getHeader("Cookie"));
如果在Servlet中輸出,必須得使用如下語句得到out,才能向客戶端瀏覽器輸出數據:
PrintWriter out = response.getWriter();
雖然永久Cookie和臨時Cookie在第二次向服務端發出HTTP請求時生成Cookie字段,但它們還是有一定的區別的。永久Cookie在任意新開啟的IE窗口都可以生成Cookie。而臨時Cookie由于只保存在當前IE窗口,因此,在新開啟的IE窗口,是不能生成Cookie字段的,也就是說,新窗口和舊窗口是不能共享臨時Cookie的。使用重定向機制彈出的新窗口也無法和舊窗口共享臨時Cookie。但在同一個窗口可以。如在一個IE窗口輸入http://localhost:8080/test/first.jsp,向內存寫入一個臨時Cookie后,在同一個IE窗口輸入http://localhost:8080/test/second.jsp,瀏覽器在向服務端發送HTTP請求時,自動將當前瀏覽器的臨時Cookie(也就是first.jsp所創建的Cookie)和永久Cookie作為HTTP請求頭的Cookie字段值發送給服務端。但是如果新啟一個IE窗口,由于新IE窗口沒有這個臨時Cookie,因此,second.jsp只發送了保存在硬盤上的永久Cookie。
二、Tomcat中的Servlet和Session
由于Cookie數存在保存在客戶端,這樣對于一些敏感數據會帶來一些風險。而且Cookie一般只能保存字符串等簡單數據。并且大小限制在4KB。如果要保存比較復雜的數據,Cookie可能顯得有些不合適。基于這些原因,我們自然會想到在服務端采用這種類似Cookie的機制來存儲數據。這就是我們這節要講的會話(Session)。而在一個客戶端和服務端的會話中所有的頁面可以共享為這個會話所建立的Session。
那么什么是會話呢?有很多人認為會話就是在一臺機器上客戶端瀏覽器訪問某個域名所指向的服務端程序,就建立了一個客戶端到服務端的會話。然后關閉客戶端瀏覽器,會話就結束。其實這并不準確。
首先讓我們先來看看Session的原理。Session和Cookie類似。所不同的是它是建立在服務端的對象。每一個Session對象一個會話。也許很多讀者看到這會有一個疑問。Session是如何同客戶端聯系在一起的呢?很多人在使用Session時并沒有感覺到這一點。其實這一切都是Web服務器,如Tomcat一手包辦的。那么Web服務器又是如何識別通過HTTP協議進行連接的客戶端的呢?這就要用到第一節中所講的Cookie。在一般情況下,Session使用了臨時Cookie來識別某一個Session是否屬于某一個會話。在本文中以Tomcat為例來說明Session是如何工作的。
讓我們先假設某一個客戶端第一次訪問一個Servlet,在這個Servlet中使用了getSession來得到一個Session對象,也就是建立了一個會話,這個Servlet的代碼如下:
import java.io.*;
import javax.servlet.ServletException;
import javax.servlet.http.*;
public class First extends HttpServlet
{
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
response.setContentType("text/html");
HttpSession session = request.getSession();
session.setAttribute("key", "mySessionValue");
PrintWriter out = response.getWriter();
out.println("The session has been generated!");
out.flush();
out.close();
}
}
對于服務端的First來說,getSession方法主要做了兩件事:
1. 從客戶端的HTTP請求頭的Cookie字段中獲得一個尋找一個JSESSIONID的key,這個key的值是一個唯一字符串,類似于D
2. 如果Cookie中包含這個JSESSIONID,將key的值取出,在Tomcat的Session Map(用于保存Tomcat自啟動以來的所有創建的Session)中查找,如果找到,將這個Session取出,如果未找到,創建一個HttpSession對象,并保存在Session Map中,以便下一次使用這個Key來獲得這個Session。
在服務器向客戶端發送響應信息時,如果是新創建的HttpSession對象,在響應HTTP
頭中加了一個Set-Cookie字段,并將JSESSIONID和相應的值反回給客戶端。如下面的HTTP響應頭:
HTTP/1.1 200 OK
...
Set-Cookie: JSESSIONID=D
...
對于客戶端瀏覽器來說,并不認識哪個Cookie是用于Session的,它只是將相應的臨時Cookie和永久Cookie原封不動地放到請求HTTP頭的Cookie字段中,發送給服務器。如果在IE中首次訪問服務端的First,這時在當前IE窗口并沒有臨時Cookie,因此,在請求HTTP頭中就沒有Cookie字段,所以First在調用getSession方法時就未找到JSESSIONID,因此,就會新建一個HttpSession對象。并在Set-Cookie中將這個JSESSIONID返回。接下來我們使用另外一個Servlet:Second來獲得在First中所設置的Session數據。Second的代碼如下:
import java.io.*;
import javax.servlet.ServletException;
import javax.servlet.http.*;
public class Second extends HttpServlet
{
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
response.setContentType("text/html");
HttpSession session = request.getSession();
PrintWriter out = response.getWriter();
out.println(session.getAttribute("key"));
out.flush();
out.close();
}
}
如果在同一個窗口來調用Second。這時客戶端已經有了一個臨時Cookie,就是JSESSIONID,因此,會將這個Cookie放到HTTP頭的Cookie字段中發送給服務端。服務端在收到這個HTTP請求時就可以從Cookie中得到JSESSIONID的值,并從Session Map中找到這個Session對象,也就是getSession方法的返回值。因此,從技術層面上來說,所有擁有同一個Session ID的頁面都應該屬于同一個會話。
如果我們在一個新的IE窗口調用Second,并不會得到mySessionValue。因為這時Second和First擁有了不同的Session ID,因此,它們并不屬于同一個會話。講到這,也許很多讀者眼前一亮。既然擁有同一個Session ID,就可以共享Session對象,那么我們可不可以使用永久Cookie將這個Session ID保存在Cookie文件中,這樣就算在新的IE窗口,也可以共享Session對象了。答案是肯定的。下面是新的First代碼:
import java.io.*;
import javax.servlet.ServletException;
import javax.servlet.http.*;
public class First extends HttpServlet
{
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
response.setContentType("text/html");
HttpSession session = request.getSession();
session.setMaxInactiveInterval(3600);
Cookie cookie = new Cookie("JSESSIONID", session.getId());
cookie.setMaxAge(3600);
response.addCookie(cookie);
session.setAttribute("key", "mySessionValue");
PrintWriter out = response.getWriter();
out.println("The session has been generated!");
out.flush();
out.close();
}
}
在上面的代碼中使用了Cookie對象將JSESSIONID寫入了Cookie文件,并使用setMaxAge方法將Cookie超時時間設為3600秒(1小時)。這樣只要訪問過First,從訪問時間算起,在1小時之內,在本機的任何IE窗口調用Second都會得到"mySessionValue"字符串。
三三、Tomcat中的JSP和Session
從本質上講,JSP在運行時已經被編譯成相應的Servlet了,因此,在JSP和Servlet中Session的使用方法應該差不多。但還是有一些細小的差別。
如果我們使用過JSP就會發現,在JSP中很多對象是不需要創建的,如out、session等。它們可以直接使用。如下面的JSP代碼所示:
<!-- MyJSP.jsp -->
<%@ page language="java" contentType="text/html; charset=GB18030"
pageEncoding="GB18030"%>
<!DOCTYPE html PUBLIC "-//W
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=GB18030">
<title>Insert title here</title>
</head>
<body>
<%
out.println(session.getId());
%>
</body>
</html>
在上面的JSP代碼中直接使用了out和session。而并不象Servlet里一樣用get方法來獲得相應的對象實例。那么這是為什么呢?
由于JSP在第一次運行時是被編譯成Servlet的,我們自然就會想到有可能是在編譯JSP時自動創建了session和out對象。下面我們就來驗證這一點。首先需要查看一下JSP被編譯成Servlet后的源代碼。這非常簡單,如果我們使用的是Tomcat,只需要在Tomcat的安裝目錄中的work中找相應的源程序即可。如一個名為MyJSP.jsp的文件首先被編譯成MyJSP_jsp.java(這就是由JSP生成的Servlet源程序文件),然后再由java將MyJSP_jsp.java編譯成MyJSP_jsp.class,最后Tomcat運行的就是MyJSP_jsp.class。如上面的JSP程序被編譯成Servlet的部分源代碼如下:
package org.apache.jsp;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;
public final class MyJSP_jsp extends org.apache.jasper.runtime.HttpJspBase
{
... ...
... ...
public void _jspService(HttpServletRequest request, HttpServletResponse response)
throws java.io.IOException, ServletException {
PageContext pageContext = null;
HttpSession session = null;
ServletContext application = null;
ServletConfig config = null;
JspWriter out = null;
Object page = this;
JspWriter _jspx_out = null;
PageContext _jspx_page_context = null;
try {
response.setContentType("text/html; charset=GB18030");
pageContext = _jspxFactory.getPageContext(this, request, response,
null, true, 8192, true);
_jspx_page_context = pageContext;
application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();
_jspx_out = out;
out.write("\r\n");
out.write("<!DOCTYPE html PUBLIC \"-//W
out.write("<html>\r\n");
out.write("\t<head>\r\n");
out.write("\t\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=GB18030\">\r\n");
out.write("\t\t<title>Insert title here</title>\r\n");
out.write("\t</head>\r\n");
out.write("\t<body>\r\n");
out.write("\t\t");
out.println(session.getId());
out.write("\r\n");
out.write("\r\n");
out.write("\t</body>\r\n");
out.write("</html>");
} catch (Throwable t) {
if (!(t instanceof SkipPageException)){
out = _jspx_out;
if (out != null && out.getBufferSize() != 0)
try { out.clearBuffer(); } catch (java.io.IOException e) {}
if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
}
} finally {
_jspxFactory.releasePageContext(_jspx_page_context);
}
}
}
我們可以看到上面的代碼中的_jspService方法類似于HttpServlet中的service方法,在方法的開始部分首先建立了session、application、out等對象實例。然后將MyJSP.jsp中的HTML通過out輸出到客戶端。我們要注意上面的黑體字的語句:out.println(session.getId());,JSP編譯器自動將JSP中的<% ... %>中包含的Java代碼原封不動地插入到_jspService中。由于是在創建對象實例后插入,因此,就可以直接使用session、out等對象了。
如果我們想做進一步的實驗,可以直接使用javac來編譯MyJSP_jsp.java,為了方便其間,首先建立一個c.cmd文件,它的內容如下:
javac -classpath
D:\tools\apache-tomcat-
其中D:\tools\apache-tomcat-6.0.13是tomcat的安裝目錄,讀者可以將其設為自己的機器上的tomcat安裝目錄
在編譯時可直接使用c MyJSP_jsp.java進行編譯,這時tomcat就直接運行我們編譯生成的MyJSP_jsp.class了。
從上面的代碼我們還可以了解一點,在JSP無論使用還是不使用session,都會使用getSession方法創建一個Session對象,而Servlet必須顯式地調用才會建立Session對象。
注:通過直接編譯java文件運行jsp,需要清除一下tomcat的緩存,一般需要重啟一下tomcat。
四、隨心所欲使用Session
(1) 使用url傳遞session id
在上面講過,在默認情況下session是依靠客戶端的cookie來實現的。但如果客戶端瀏覽器不支持cookie或將cookie功能關閉,那就就意味著無法通過cookie來實現session了。在這種情況下,我們還可以有另一種選擇,就是通過url來傳遞session id。
對于Tomcat來說,需要使用jsessionid作為key來傳遞session id。但具體如何傳呢?可能有很多人認為會是如下的格式:
http://localhost:8080/test/MyJSP.jsp?jsessionid= D
但實驗上面的url并不好使。其實最直接的方法我們可以看一下Tomcat的源程序是如何寫的,首先下載tomcat的源程序,然后找到CoyoteAdapter.java文件,并打開。在其中找到parseSessionId方法,這個方法是用來從url中提取Session id的。我們可以不必了解這個方法的全部代碼,只看一下開頭就可以。代碼片段如下:
ByteChunk uriBC = req.requestURI().getByteChunk();
int semicolon = uriBC.indexOf(match, 0, match.length(), 0);
if (semicolon > 0) {...}
上面代碼中的uriBC就是請求的url,第二行在這個url中查找match字符串,再在CoyoteAdapter.java中查找一個match字符串,match變量的初值如下:
private static final String match =
";" + Globals. SESSION_PARAMETER_NAME + "=";
從上面代碼可以看出,match開頭是一個";"字符,而SESSION_PARAMETER_NAME是一個常量,值就是"jsessionid",因此可以斷定,MyJSP.jsp后跟的是";",并不是"?",因此,正確的url如下:
http://localhost:8080/test/MyJSP.jsp;jsessionid= D
通過使用上述方法甚至可以在不同的機器上獲得同一個session對象。
在CoyoteAdapter.java文件中還有一個parseSessionCookiesId方法,這個方法將從HTTP請求頭中提取session id。我們中postParseRequest方法中可以看到將調用的parseSessionId方法,在最后調用了parseSessionCookiesId方法,因此,我們可以斷定,tomcat將考慮url中的session id,然后再讀取Cookie字段中的session id。還有就是在postParseRequest方法的最后部分有一個response.sendRedirect(redirectPath);,在調完它后,就直接return了。而沒有執行到parseSessionCookiesId,因此,使用重定向并不能通過HTTP頭的cookie字段共享session。只能通過url來傳遞session id。
(2) 將tomcat的cookie支持關閉
如果我們只想使用url來支持session,可以直接將tomcat的cookie功能關閉。我們可
以修改conf中的context.xml文件,加入一個cookies="false"即可,內容如下:
<!-- The contents of this file will be loaded for each web application -->
<Context cookies = "false">
... ...
... ...
</Context>
重啟tomcat后,就算客戶端支持cookie,tomcat也不會考慮HTTP請求頭的cookie字段。
(3) 在IE中控制Cookie
在IE中也可以將Cookie關閉,啟動IE,在工具->Internet選項->穩私->高級中選中"覆蓋自動cookie處理"選項。并按圖1選擇:
圖1
對于下面的選項"總是允許會話cookie",如果不選,在本機將允許會話cookie,也就是通過localhost訪問,在遠程將不允許會話cookie。我們也可以通過在工具->Internet選項->穩私->站點來對某個網站來允許和拒絕cookie。
posted on 2009-08-10 10:31 肥仔 閱讀(3174) 評論(0) 編輯 收藏 引用 所屬分類: Web-后臺