會話(Session)跟蹤是Web程序中常用的技術,用來跟蹤用戶的整個會話。常用的會話跟蹤技術是Cookie與Session。Cookie通過在客戶端記錄信息確定用戶身份,Session通過在服務器端記錄信息確定用戶身份。本章將系統地講述Cookie與Session機制,並比較說明什麼時候不能用Cookie,什麼時候不能用Session。
本章的所有源代碼均包含在項目Session中。
5.1 Cookie機制
在程序中,會話跟蹤是很重要的事情。理論上,一個用戶的所有請求操作都應該屬於同一個會話,而另一個用戶的所有請求操作則應該屬於另一個會話,二者不能混淆。例如,用戶A在超市購買的任何商品都應該放在A的購物車內,不論是用戶A什麼時間購買的,這都是屬於同一個會話的,不能放入用戶B或用戶C的購物車內,這不屬於同一個會話。
而Web應用程序是使用HTTP協議傳輸數據的。HTTP協議是無狀態的協議。一旦數據交換完畢,客戶端與服務器端的連接就會關閉,再次交換數據需要建立新的連接。這就意味着服務器無法從連接上跟蹤會話。即用戶A購買了一件商品放入購物車內,當再次購買商品時服務器已經無法判斷該購買行爲是屬於用戶A的會話還是用戶B的會話了。要跟蹤該會話,必須引入一種機制。
Cookie就是這樣的一種機制。它可以彌補HTTP協議無狀態的不足。在Session出現之前,基本上所有的網站都採用Cookie來跟蹤會話。
5.1.1 什麼是Cookie
Cookie意爲“甜餅”,是由W3C組織提出,最早由Netscape社區發展的一種機制。目前Cookie已經成爲標準,所有的主流瀏覽器如IE、Netscape、Firefox、Opera等都支持Cookie。
由於HTTP是一種無狀態的協議,服務器單從網絡連接上無從知道客戶身份。怎麼辦呢?就給客戶端們頒發一個通行證吧,每人一個,無論誰訪問都必須攜帶自己通行證。這樣服務器就能從通行證上確認客戶身份了。這就是Cookie的工作原理。
Cookie實際上是一小段的文本信息。客戶端請求服務器,如果服務器需要記錄該用戶狀態,就使用response向客戶端瀏覽器頒發一個Cookie。客戶端瀏覽器會把Cookie保存起來。當瀏覽器再請求該網站時,瀏覽器把請求的網址連同該Cookie一同提交給服務器。服務器檢查該Cookie,以此來辨認用戶狀態。服務器還可以根據需要修改Cookie的內容。
查看某個網站頒發的Cookie很簡單。在瀏覽器地址欄輸入javascript:alert(document. cookie)就可以了。JavaScript腳本會彈出一個對話框顯示本網站頒發的所有Cookie的內容,如圖5.1所示。
圖5.1 Baidu網站頒發的Cookie
圖5.1中彈出的對話框中顯示的爲Baidu網站的Cookie。其中第一行BAIDUID記錄的就是筆者的身份helloweenvsfei,只是Baidu使用特殊的方法將Cookie信息加密了。
%注意:Cookie功能需要瀏覽器的支持。如果瀏覽器不支持Cookie(如大部分手機中的瀏覽器)或者把Cookie禁用了,Cookie功能就會失效。不同的瀏覽器採用不同的方式保存Cookie。IE瀏覽器會在“C:\Documents and Settings\你的用戶名\Cookies”文件夾下以文本文件形式保存,一個文本文件保存一個Cookie。
5.1.2 記錄用戶訪問次數
Java中把Cookie封裝成了javax.servlet.http.Cookie類。每個Cookie都是該Cookie類的對象。服務器通過操作Cookie類對象對客戶端Cookie進行操作。通過request.getCookie()獲取客戶端提交的所有Cookie(以Cookie[]數組形式返回),通過response.addCookie(Cookiecookie)向客戶端設置Cookie。
Cookie對象使用key-value屬性對的形式保存用戶狀態,一個Cookie對象保存一個屬性對,一個request或者response同時使用多個Cookie。因爲Cookie類位於包javax.servlet.http.*下面,所以JSP中不需要import該類。
看一個使用Cookie記錄用戶賬號以及登錄次數的例子。在MyEclipse中新建WebProject,選擇Java EE 5.0規範,填寫項目名稱爲sessionWeb。新建JSP頁面cookie.jsp,輸入源代碼如下:
代碼5.1 cookie.jsp
<%@ page language="java"pageEncoding="UTF-8" errorPage="login.jsp" %>
<%
request.setCharacterEncoding("UTF-8"); // 設置request編碼
String username =""; // 用戶名
int visitTimes = 0; // 訪問次數
Cookie[] cookies =request.getCookies(); // 所有的Cookie
for(int i=0;cookies!=null&&i<cookies.length; i++){
// 遍歷Cookie尋找賬號與登錄次數
Cookie cookie =cookies[i]; // 第i個Cookie
if("username".equals(cookie.getName())){// 如果Cookie名爲username
username = cookie.getValue(); // 則記錄該Cookie的內容
}
elseif("visitTimes".equals(cookie.getName())){
// 如果Cookie名爲visitTimes
visitTimes = Integer.parseInt(cookie.getValue());
// 則記錄Cookie的內容
}
}
if(username == null ||username.trim().equals("")){
// 如果沒有找到用戶名,則轉到登錄界面
throw newException("您還沒有登錄。請先登錄");
}
// 修改 Cookie,更新用戶的訪問次數
Cookie visitTimesCookie = newCookie("visitTimes", Integer.toString
(++visitTimes));
response.addCookie(visitTimesCookie); // 覆蓋名爲visitTimes的Cookie
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01Transitional//EN">
<html>
<body>
<divalign="center" style="margin:10px; ">
<fieldset>
<legend>登錄信息</legend>
<form action="login.jsp"method="post">
<table>
<tr>
<td>您的賬號: </td>
<td><%= username %></td>
</tr>
<tr>
<td>登錄次數: </td>
<td><%= visitTimes %></td>
</tr>
<tr>
<td></td>
<td>
<input type="button" value=" 刷 新" οnclick=
"location='<%=request.getRequestURI() %>?ts=' +
new Date().getTime(); "class="button">
</td>
</tr>
</table>
</form>
</fieldset>
</div>
</body>
</html>
程序使用Cookie記錄用戶的訪問次數。如果用戶沒有登錄,則顯示登錄界面。工作原理是程序先檢查Cookie,如果沒有找到包含username屬性的Cookie,則拋出異常,頁面跳轉到errorPage指定的錯誤處理頁面login.jsp。login.jsp源代碼如下:
代碼5.2 login.jsp
<%@ page language="java"pageEncoding="UTF-8" isErrorPage="true" %>
<%
request.setCharacterEncoding("UTF-8"); // 設置request編碼方式
response.setCharacterEncoding("UTF-8"); // 設置response編碼方式
if("POST".equals(request.getMethod())){ // 如果是以POST方式登錄
CookieusernameCookie = // 新建名爲username的Cookie
newCookie("username", request.getParameter("username"));
CookievisittimesCookie = new Cookie("visitTimes", "0");
// 新建Cookie
response.addCookie(usernameCookie); // 添加到response中
response.addCookie(visittimesCookie); // response會將Cookie發送
給客戶端
response.sendRedirect(request.getContextPath() +"/cookie.jsp"); // 顯示Cookie頁面
return;
}
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01Transitional//EN">
<html>
<head>
<title>請先登錄</title>
<link rel="stylesheet"type="text/css" href="css/style.css">
</head>
<body>
<div align="center" style="margin:10px;">
<fieldset>
<legend>登錄</legend>
<formaction="login.jsp" method="post">
<table>
<tr>
<td></td>
<td><span><imgsrc="images/errorstate.gif"></span>
<spanstyle="color:red; "><%= exception.get
Message() %></span></td>
</tr>
<tr>
<td>賬號: </td>
<td><input type="text"name="username" style="width:
200px; "></td>
</tr>
<tr>
<td>密碼: </td>
<td><inputtype="password" name="password" style=
"width:200px; "></td>
</tr>
<tr>
<td></td>
<td><inputtype="submit" value=" 登 錄 " class=
"button"></td>
</tr>
</table>
</form>
</fieldset>
</div>
</body>
</html>
程序運行效果如圖5.2所示。
圖5.2 使用Cookie記錄用戶訪問次數
客戶端A與客戶端B都可能訪問該程序,A會提交A的Cookie,B會提交B的Cookie。代碼request.getCookies()並沒有指明獲取誰的Cookie。這句代碼取的是誰的Cookie呢?答案是A執行時取的是A的Cookie,B執行時取的是B的Cookie。這是Cookie機制規定的。程序只需要簡單執行request.getCookies()就可以了,服務器只會返回當前客戶的Cookie,而不會返回其他客戶的Cookie。各客戶端的Cookie彼此獨立,互不可見。
5.1.3 Cookie的不可跨域名性
很多網站都會使用Cookie。例如,Google會向客戶端頒發Cookie,Baidu也會向客戶端頒發Cookie。那瀏覽器訪問Google會不會也攜帶上Baidu頒發的Cookie呢?或者Google能不能修改Baidu頒發的Cookie呢?
答案是否定的。Cookie具有不可跨域名性。根據Cookie規範,瀏覽器訪問Google只會攜帶Google的Cookie,而不會攜帶Baidu的Cookie。Google也只能操作Google的Cookie,而不能操作Baidu的Cookie。
Cookie在客戶端是由瀏覽器來管理的。瀏覽器能夠保證Google只會操作Google的Cookie而不會操作Baidu的Cookie,從而保證用戶的隱私安全。瀏覽器判斷一個網站是否能操作另一個網站Cookie的依據是域名。Google與Baidu的域名不一樣,因此Google不能操作Baidu的Cookie。
需要注意的是,雖然網站images.google.com與網站www.google.com同屬於Google,但是域名不一樣,二者同樣不能互相操作彼此的Cookie。
%注意:用戶登錄網站www.google.com之後會發現訪問images.google.com時登錄信息仍然有效,而普通的Cookie是做不到的。這是因爲Google做了特殊處理。本章後面也會對Cookie做類似的處理。
5.1.4 Unicode編碼:保存中文
中文與英文字符不同,中文屬於Unicode字符,在內存中佔4個字符,而英文屬於ASCII字符,內存中只佔2個字節。Cookie中使用Unicode字符時需要對Unicode字符進行編碼,否則會亂碼。編碼可以使用java.net.URLEncoder類的encode(Stringstr, String encoding)方法,解碼使用java.net.URLDecoder類的decode(Stringstr, String encoding)方法,例如:
代碼5.3 encoding.jsp
<%@ page language="java"pageEncoding="UTF-8" %>
<jsp:directive.pageimport="java.net.URLEncoder"/>
<jsp:directive.pageimport="java.net.URLDecoder"/>
<%
// 使用中文的 Cookie. name 與 value 都使用UTF-8 編碼
Cookie cookie = new Cookie(
URLEncoder.encode("姓名","UTF-8"),
URLEncoder.encode("劉京華","UTF-8"));
response.addCookie(cookie); // 發送到客戶端
%>
<!DOCTYPEHTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>CookieEncoding</title>
</head>
<body>
<%
if(request.getCookies() != null){
for(Cookie cc : request.getCookies()){ // 遍歷所有的Cookie
String cookieName =URLDecoder.decode(cc.getName(), "UTF-8");
String cookieValue =URLDecoder.decode(cc.getValue(), "UTF-8");
out.println(cookieName + "=" +cookieValue + "; <br/>");
}
}
else{
out.println("Cookie 已經寫入客戶端. 請刷新頁面.");
}
%>
</body>
</html>
程序使用UTF-8編碼了Cookie內容,然後再使用UTF-8解碼Cookie並顯示出來。程序運行效果如圖5.3所示。
圖5.3 Cookie的UTF-8編碼
%提示:Cookie中保存中文只能編碼。一般使用UTF-8編碼即可。不推薦使用GBK等中文編碼,因爲瀏覽器不一定支持,而且JavaScript也不支持GBK編碼。
5.1.5 BASE64編碼:保存二進制圖片
Cookie不僅可以使用ASCII字符與Unicode字符,還可以使用二進制數據。例如在Cookie中使用數字證書,提供安全度。使用二進制數據時也需要進行編碼。下面的例子使用BASE64編碼在Cookie中保存二進制文件。源代碼如下:
代碼5.4 base64.jsp
<%@ page language="java"pageEncoding="UTF-8" %>
<jsp:directive.pageimport="sun.misc.BASE64Encoder"/>
<jsp:directive.pageimport="java.io.InputStream"/>
<jsp:directive.pageimport="java.io.File"/>
<%
File file = newFile(this.getServletContext().getRealPath("cookie.
gif"));
byte[] binary = newbyte[(int)file.length()]; // 二進制數組
// 從圖片文件讀取二進制數據.
InputStream ins =this.getServletContext().getResourceAsStream
(file.getName());
ins.read(binary);
ins.close();
String content =BASE64Encoder.class.newInstance().encode(binary);
// BASE64 編碼
Cookie cookie = newCookie("file", content);
// 包含二進制數據的 Cookie
response.addCookie(cookie); // 將 Cookie 發送到客戶端
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01Transitional//EN">
<html>
<head>
<title>Cookie Encoding</title>
</head>
<body>
從 Cookie 中獲取到的二進制圖片:<img src="base64_decode.jsp" /><br/>
<textarea id='cookieArea' style='width:100%;height:200px; '></textarea>
<scripttype="text/javascript">cookieArea.value=document.cookie;</script>
</body>
</html>
程序使用的二進制數據來自根目錄下的cookie.gif文件。程序先將二進制數據寫進客戶端Cookie中,然後又將Cookie中的二進制數據還原並顯示。輸入框中用JavaScript程序顯示了客戶端Cookie的所有內容。
解碼並顯示圖片的源代碼如下:
代碼5.5 base64_decode.jsp
<%@ page language="java"pageEncoding="UTF-8" %>
<jsp:directive.pageimport="sun.misc.BASE64Decoder"/>
<jsp:directive.pagetrimDirectiveWhitespaces="true"/>
<%
out.clear(); // 清除輸出
for(Cookie cookie : request.getCookies()){ // 遍歷Cookie
if(cookie.getName().equals("file")){ // 找到名爲file的Cookie
byte[] binary =
BASE64Decoder.class.newInstance().decodeBuffer(cookie.
getValue().replace(" ", "")); // 解碼BASE64編碼的二進制內容
response.setHeader("Content-Type", "image/gif");
// 設置內容類型爲 gif圖片
response.setHeader("Content-Disposition", "inline;
filename=cookie.gif");
response.setHeader("Connection", "close");
response.setContentLength(binary.length); // 設置輸出內容的長度
response.getOutputStream().write(binary); // 輸出到客戶端
response.getOutputStream().flush(); // 清空緩存
response.getOutputStream().close(); // 關閉輸出流
return;
}
}
%>
程序運行效果如圖5.4所示。選中的部分爲二進制Cookie編碼後的內容。輸入框上方的小圖標就是解密後Cookie顯示的圖片。
圖5.4 Cookie中的二進制數據
%注意:本程序僅用於展示Cookie中可以存儲二進制內容,並不實用。由於瀏覽器每次請求服務器都會攜帶Cookie,因此Cookie內容不宜過多,否則影響速度。Cookie的內容應該少而精。
5.1.6 設置Cookie的所有屬性
除了name與value之外,Cookie還具有其他幾個常用的屬性。每個屬性對應一個getter方法與一個setter方法。Cookie類的所有屬性如表5.1所示。
表5.1 Cookie常用屬性
屬 性 名 |
描 述 |
String name |
該Cookie的名稱。Cookie一旦創建,名稱便不可更改 |
Object value |
該Cookie的值。如果值爲Unicode字符,需要爲字符編碼。如果值爲二進制數據,則需要使用BASE64編碼 |
int maxAge |
該Cookie失效的時間,單位秒。如果爲正數,則該Cookie在maxAge秒之後失效。如果爲負數,該Cookie爲臨時Cookie,關閉瀏覽器即失效,瀏覽器也不會以任何形式保存該Cookie。如果爲0,表示刪除該Cookie。默認爲–1 |
boolean secure |
該Cookie是否僅被使用安全協議傳輸。安全協議。安全協議有HTTPS,SSL等,在網絡上傳輸數據之前先將數據加密。默認爲false |
String path |
該Cookie的使用路徑。如果設置爲“/sessionWeb/”,則只有contextPath爲“/sessionWeb”的程序可以訪問該Cookie。如果設置爲“/”,則本域名下contextPath都可以訪問該Cookie。注意最後一個字符必須爲“/” |
String domain |
可以訪問該Cookie的域名。如果設置爲“.google.com”,則所有以“google.com”結尾的域名都可以訪問該Cookie。注意第一個字符必須爲“.” |
String comment |
該Cookie的用處說明。瀏覽器顯示Cookie信息的時候顯示該說明 |
int version |
該Cookie使用的版本號。0表示遵循Netscape的Cookie規範,1表示遵循W3C的RFC 2109規範 |
下面是一個使用所有參數設置Cookie的一個例子。源代碼如下:
代碼5.6 setCookie.jsp
<%@ page language="java"pageEncoding="UTF-8" %>
<jsp:directive.pageimport="java.net.URLEncoder"/>
<%!
boolean isNull(String str){ // 返回字符串是否爲空
return str==null ||str.trim().length()==0;
}
%>
<%
request.setCharacterEncoding("UTF-8"); // 設置request編碼
if("POST".equals(request.getMethod())){ // 如果是POST提交數據
String name =request.getParameter("name"); // 獲取name參數
String value =request.getParameter("value"); // 獲取value參數
String maxAge =request.getParameter("maxAge"); // 獲取maxAge參數
String domain =request.getParameter("domain"); // 獲取domain參數
String path =request.getParameter("path"); // 獲取path參數
String comment =request.getParameter("comment"); // 獲取comment參數
String secure =request.getParameter("secure"); // 獲取secure參數
if(!isNull(name)){ // 如果name參數不爲空
Cookie cookie = new Cookie( // 則生成新的Cookie
URLEncoder.encode(name, "UTF-8"),
URLEncoder.encode(value, "UTF-8"));
// 若maxAge非空則設置maxAge屬性
if(!isNull(maxAge)) cookie.setMaxAge(Integer.parseInt(maxAge));
if(!isNull(domain)) cookie.setDomain(domain);
// 若domain非空則設置domain
if(!isNull(path)) cookie.setPath(path);
// 若path非空則設置path
if(!isNull(comment)) cookie.setComment(comment);
// 設置comment
if(!isNull(secure))cookie.setSecure("true".equalsIgnoreCase(secure));
response.addCookie(cookie); // 覆蓋舊的Cookie
}
}
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01Transitional//EN">
<html>
<head>
<title>Cookie</title>
<meta http-equiv="pragma"content="no-cache">
<meta http-equiv="cache-control"content="no-cache">
<meta http-equiv="expires"content="0">
<meta http-equiv="keywords"content="keyword1,keyword2,keyword3">
<meta http-equiv="description"content="This is my page">
<link rel="stylesheet"type="text/css" href="css/style.css">
</head>
<body>
<div align="center" style="margin:10px;">
<fieldset>
<legend>當前有效的 Cookie</legend>
<scripttype="text/javascript">
document.write(document.cookie);
</script>
</fieldset>
<fieldset>
<legend>設置新 Cookie</legend>
<formaction="setCookie.jsp" method="POST">
<table>
<tr><td>name: </td>
<td><input name="name"type="text" style="width:200px; ">
</td>
</tr>
<tr><td>value: </td>
<td><inputname="value" type="text" style="width:200px;">
</td>
</tr>
<tr><td>maxAge: </td>
<td><inputname="maxAge" type="text" style="width:
200px; "></td>
</tr>
<tr><td>domain: </td>
<td><inputname="domain" type="text" style="width:
200px; "></td>
</tr>
<tr><td>path: </td>
<td><input name="path"type="text" style="width:200px; ">
</td>
</tr>
<tr><td>comment: </td>
<td><inputname="comment" type="text" style="width:
200px; "></td>
</tr>
<tr><td>secure: </td>
<td><inputname="secure" type="text" style="width:
200px; "></td>
</tr>
<tr><td></td>
<td><inputtype="submit" value=" 提 交 " class=
"button">
<input type="button" value=" 刷 新 " οnclick=
"location='setCookie.jsp'">
</td>
</tr>
</table>
</form>
</fieldset>
</div>
</body>
</html>
程序運行效果如圖5.5所示。
圖5.5 動態設置Cookie
程序顯示了所有的有效Cookie,並且可以使用不同參數創建Cookie,體會Cookie屬性的用處。例如,將maxAge設爲0可以刪除同名的Cookie。注意JSESSIONID這個Cookie是Tomcat自動生成的。
%提示:本例是一個演示程序,可以創建不同屬性的Cookie。各屬性用處請看下文。
5.1.7 Cookie的有效期
Cookie的maxAge決定着Cookie的有效期,單位爲秒(Second)。Cookie中通過getMaxAge()方法與setMaxAge(intmaxAge)方法來讀寫maxAge屬性。
如果maxAge屬性爲正數,則表示該Cookie會在maxAge秒之後自動失效。瀏覽器會將maxAge爲正數的Cookie持久化,即寫到對應的Cookie文件中。無論客戶關閉了瀏覽器還是電腦,只要還在maxAge秒之前,登錄網站時該Cookie仍然有效。下面代碼中的Cookie信息將永遠有效。
Cookie cookie = new Cookie("username","helloweenvsfei"); // 新建Cookie
cookie.setMaxAge(Integer.MAX_VALUE); // 設置生命週期爲MAX_VALUE
response.addCookie(cookie); // 輸出到客戶端
如果maxAge爲負數,則表示該Cookie僅在本瀏覽器窗口以及本窗口打開的子窗口內有效,關閉窗口後該Cookie即失效。maxAge爲負數的Cookie,爲臨時性Cookie,不會被持久化,不會被寫到Cookie文件中。Cookie信息保存在瀏覽器內存中,因此關閉瀏覽器該Cookie就消失了。Cookie默認的maxAge值爲–1。
如果maxAge爲0,則表示刪除該Cookie。Cookie機制沒有提供刪除Cookie的方法,因此通過設置該Cookie即時失效實現刪除Cookie的效果。失效的Cookie會被瀏覽器從Cookie文件或者內存中刪除,例如:
Cookie cookie = new Cookie("username","helloweenvsfei"); // 新建Cookie
cookie.setMaxAge(0); // 設置生命週期爲0,不能爲負數
response.addCookie(cookie); // 必須執行這一句
response對象提供的Cookie操作方法只有一個添加操作add(Cookiecookie)。要想修改Cookie只能使用一個同名的Cookie來覆蓋原來的Cookie,達到修改的目的。刪除時只需要把maxAge修改爲0即可。
%注意:從客戶端讀取Cookie時,包括maxAge在內的其他屬性都是不可讀的,也不會被提交。瀏覽器提交Cookie時只會提交name與value屬性。maxAge屬性只被瀏覽器用來判斷Cookie是否過期。
5.1.8 Cookie的修改、刪除
Cookie並不提供修改、刪除操作。如果要修改某個Cookie,只需要新建一個同名的Cookie,並添加到response中覆蓋原來的Cookie。
如果要刪除某個Cookie,只需要新建一個同名的Cookie,並將maxAge設置爲0,並添加到response中覆蓋原來的Cookie。注意是0而不是負數。負數代表其他的意義。讀者可以通過上例的程序進行驗證,設置不同的屬性。
%注意:修改、刪除Cookie時,新建的Cookie除value、maxAge之外的所有屬性,例如name、path、domain等,都要與原Cookie完全一樣。否則,瀏覽器將視爲兩個不同的Cookie不予覆蓋,導致修改、刪除失敗。
5.1.9 Cookie的域名
Cookie是不可跨域名的。域名www.google.com頒發的Cookie不會被提交到域名www.baidu.com去。這是由Cookie的隱私安全機制決定的。隱私安全機制能夠禁止網站非法獲取其他網站的Cookie。
正常情況下,同一個一級域名下的兩個二級域名如www.helloweenvsfei.com和images.helloweenvsfei.com也不能交互使用Cookie,因爲二者的域名並不嚴格相同。如果想所有helloweenvsfei.com名下的二級域名都可以使用該Cookie,需要設置Cookie的domain參數,例如:
Cookie cookie = new Cookie("time","20080808"); // 新建Cookie
cookie.setDomain(".helloweenvsfei.com"); // 設置域名
cookie.setPath("/"); // 設置路徑
cookie.setMaxAge(Integer.MAX_VALUE); // 設置有效期
response.addCookie(cookie); // 輸出到客戶端
讀者可以修改本機C:\WINDOWS\system32\drivers\etc下的hosts文件來配置多個臨時域名,然後使用setCookie.jsp程序來設置跨域名Cookie驗證domain屬性。
注意:domain參數必須以點(".")開始。另外,name相同但domain不同的兩個Cookie是兩個不同的Cookie。如果想要兩個域名完全不同的網站共有Cookie,可以生成兩個Cookie,domain屬性分別爲兩個域名,輸出到客戶端。
5.1.10 Cookie的路徑
domain屬性決定運行訪問Cookie的域名,而path屬性決定允許訪問Cookie的路徑(ContextPath)。例如,如果只允許/sessionWeb/下的程序使用Cookie,可以這麼寫:
Cookie cookie = new Cookie("time","20080808"); // 新建Cookie
cookie.setPath("/session/"); // 設置路徑
response.addCookie(cookie); // 輸出到客戶端
設置爲“/”時允許所有路徑使用Cookie。path屬性需要使用符號“/”結尾。name相同但domain相同的兩個Cookie也是兩個不同的Cookie。
%注意:頁面只能獲取它屬於的Path的Cookie。例如/session/test/a.jsp不能獲取到路徑爲/session/abc/的Cookie。使用時一定要注意。
5.1.11 Cookie的安全屬性
HTTP協議不僅是無狀態的,而且是不安全的。使用HTTP協議的數據不經過任何加密就直接在網絡上傳播,有被截獲的可能。使用HTTP協議傳輸很機密的內容是一種隱患。如果不希望Cookie在HTTP等非安全協議中傳輸,可以設置Cookie的secure屬性爲true。瀏覽器只會在HTTPS和SSL等安全協議中傳輸此類Cookie。下面的代碼設置secure屬性爲true:
Cookie cookie = new Cookie("time","20080808"); // 新建Cookie
cookie.setSecure(true); // 設置安全屬性
response.addCookie(cookie); // 輸出到客戶端
%提示:secure屬性並不能對Cookie內容加密,因而不能保證絕對的安全性。如果需要高安全性,需要在程序中對Cookie內容加密、解密,以防泄密。
5.1.12 JavaScript操作Cookie
Cookie是保存在瀏覽器端的,因此瀏覽器具有操作Cookie的先決條件。瀏覽器可以使用腳本程序如JavaScript或者VBScript等操作Cookie。這裏以JavaScript爲例介紹常用的Cookie操作。例如下面的代碼會輸出本頁面所有的Cookie。
<script>document.write(document.cookie);</script>
各Cookie之間用分號“;”隔開,例如:“cookie1=A;cookie2=B”。JavaScript中並沒有專門處理Cookie的API,如果想單獨獲取某個Cookie值,只能手工寫代碼解析字符串。例如下面代碼中的setCookie()與getCookie()方法。
代碼5.7 javascript.jsp
<%@ page language="java"pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01Transitional//EN">
<html>
<head>
<scripttype="text/javascript">
functiongetCookie(name){ // 返回名爲name的Cookie
var str = document.cookie; // 獲取Cookie字符串
if(!str || str.indexOf(name + "=") < 0) // 尋找name=
return;
var cookies = str.split("; "); // 用;將所有的Cookie分隔開
for(var i=0; i<cookies.length; i++){ // 遍歷每個Cookie
var cookie = cookies[i]; // 當前Cookie
if(cookie.indexOf(name + "=") ==0){ // 如果名字爲name
var value = cookie.substring(name.length+ 1);
// 獲取value
returndecodeURI(value); // 將value解碼,並返回
}
}
}
functionsetCookie(name, value){ // 設置Cookie
document.cookie = name + "=" + encodeURI(value);
// 直接設置即可
}
</script>
</head>
<body>
<divalign="center" style="margin:10px; ">
<fieldset>
<legend>當前有效的 Cookie</legend>
<div id="cookieDiv"></div>
<script type="text/javascript">
cookieDiv.innerHTML = document.cookie;
</script>
</fieldset>
<fieldset>
<legend>歡迎您</legend>
<table>
<tr>
<td>讀取 Cookie: </td>
<td><inputname="name1" /> <input class="button" type="button" value="讀取" οnclick="alert
(getCookie(name1.value));"></td>
</tr>
<tr>
<td>設置 Cookie: </td>
<td></td>
</tr>
<tr>
<td align="right">Name屬性: </td>
<td><inputname="name2" /></td>
</tr>
<tr>
<tdalign="right">Value 屬性: </td>
<td><inputname="value2" /></td>
</tr>
<tr>
<td> </td>
<td><inputtype="button" value="設置" οnclick=
"setCookie(name2.value,value2.value); cookieDiv.
innerHTML = document.cookie; "class="button"></td>
</tr>
</table>
</fieldset>
</div>
</body>
</html>
代碼使用純JavaScript代碼實現了讀寫Cookie。運行效果如圖5.6所示。
圖5.6 JavaScript讀寫Cookie
上面的setCookie()是個簡化了的方法,只能設置name與value屬性。如果要設置所有的Cookie屬性,可以使用下面的完全版本。
function setCookie(name, value){ // 設置Cookie
var expires = (arguments.length >2) ? arguments[2] : null;
// 判斷expires屬性
var path = (arguments.length > 3) ?arguments[3] : null;
// 判斷path屬性
var domain = (arguments.length > 4)? arguments[4] : null;
// 判斷domain屬性
var secure = (arguments.length > 5)? arguments[5] : false;
// 判斷secure屬性
document.cookie = name + "="+ encodeURI(value) + // 設置Cookie屬性
((expires == null) ?"" : ("; expires=" + expires.toGMTString())) +
((path == null) ?"" : ("; path=" + path)) +
((domain == null) ?"" : ("; domain=" + domain)) +
((secure == true) ?"; secure" : "");
}
由於JavaScript能夠任意地讀寫Cookie,有些好事者便想使用JavaScript程序去窺探用戶在其他網站的Cookie。不過這是徒勞的,W3C組織早就意識到JavaScript對Cookie的讀寫所帶來的安全隱患並加以防備了,W3C標準的瀏覽器會阻止JavaScript讀寫任何不屬於自己網站的Cookie。換句話說,A網站的JavaScript程序讀寫B網站的Cookie不會有任何結果。
5.1.13 案例:永久登錄
如果用戶是在自己家的電腦上上網,登錄時就可以記住他的登錄信息,下次訪問時不需要再次登錄,直接訪問即可。實現方法是把登錄信息如賬號、密碼等保存在Cookie中,並控制Cookie的有效期,下次訪問時再驗證Cookie中的登錄信息即可。
保存登錄信息有多種方案。最直接的是把用戶名與密碼都保持到Cookie中,下次訪問時檢查Cookie中的用戶名與密碼,與數據庫比較。這是一種比較危險的選擇,一般不把密碼等重要信息保存到Cookie中。
還有一種方案是把密碼加密後保存到Cookie中,下次訪問時解密並與數據庫比較。這種方案略微安全一些。如果不希望保存密碼,還可以把登錄的時間戳保存到Cookie與數據庫中,到時只驗證用戶名與登錄時間戳就可以了。
這幾種方案驗證賬號時都要查詢數據庫。本例將採用另一種方案,只在登錄時查詢一次數據庫,以後訪問驗證登錄信息時不再查詢數據庫。實現方式是把賬號按照一定的規則加密後,連同賬號一塊保存到Cookie中。下次訪問時只需要判斷賬號的加密規則是否正確即可。本例把賬號保存到名爲account的Cookie中,把賬號連同密鑰用MD5算法加密後保存到名爲ssid的Cookie中。驗證時驗證Cookie中的賬號與密鑰加密後是否與Cookie中的ssid相等。相關代碼如下:
代碼5.8 loginCookie.jsp
<%@ page language="java"pageEncoding="UTF-8" isErrorPage="false" %>
<%! // JSP方法
private static final String KEY =":[email protected]";
// 密鑰
public final static StringcalcMD5(String ss) { // MD5 加密算法
String s = ss==null ?"" : ss; // 若爲null返回空
char hexDigits[] = { '0','1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f' }; // 字典
try {
byte[] strTemp =s.getBytes(); // 獲取字節
MessageDigestmdTemp = MessageDigest.getInstance("MD5"); // 獲取MD5
mdTemp.update(strTemp); // 更新數據
byte[] md =mdTemp.digest(); // 加密
int j =md.length; // 加密後的長度
char str[] = newchar[j * 2]; // 新字符串數組
int k =0; // 計數器k
for (int i = 0; i< j; i++) { // 循環輸出
byte byte0 =md[i];
str[k++] =hexDigits[byte0 >>> 4 & 0xf];
str[k++] =hexDigits[byte0 & 0xf];
}
return newString(str); // 加密後字符串
} catch (Exception e){return null; }
}
%>
<%
request.setCharacterEncoding("UTF-8"); // 設置request編碼
response.setCharacterEncoding("UTF-8"); // 設置response編碼
String action =request.getParameter("action"); // 獲取action參數
if("login".equals(action)){ // 如果爲login動作
String account =request.getParameter("account");
// 獲取account參數
String password =request.getParameter("password");
// 獲取password參數
int timeout = newInteger(request.getParameter("timeout"));
// 獲取timeout參數
String ssid =calcMD5(account + KEY); // 把賬號、密鑰使用MD5加密後保存
CookieaccountCookie = new Cookie("account", account);
// 新建Cookie
accountCookie.setMaxAge(timeout); // 設置有效期
Cookie ssidCookie =new Cookie("ssid", ssid); // 新建Cookie
ssidCookie.setMaxAge(timeout); // 設置有效期
response.addCookie(accountCookie); // 輸出到客戶端
response.addCookie(ssidCookie); // 輸出到客戶端
// 重新請求本頁面,參數中帶有時間戳,禁止瀏覽器緩存頁面內容
response.sendRedirect(request.getRequestURI() + "?" + System.
currentTimeMillis());
return;
}
elseif("logout".equals(action)){ // 如果爲logout動作
CookieaccountCookie = new Cookie("account", "");
// 新建Cookie,內容爲空
accountCookie.setMaxAge(0); // 設置有效期爲0,刪除
Cookie ssidCookie =new Cookie("ssid", ""); // 新建Cookie,內容爲空
ssidCookie.setMaxAge(0); // 設置有效期爲0,刪除
response.addCookie(accountCookie); // 輸出到客戶端
response.addCookie(ssidCookie); // 輸出到客戶端
//重新請求本頁面,參數中帶有時間戳,禁止瀏覽器緩存頁面內容
response.sendRedirect(request.getRequestURI() + "?" + System.
currentTimeMillis());
return;
}
boolean login = false; // 是否登錄
String account = null; // 賬號
String ssid = null; // SSID標識
if(request.getCookies() !=null){ // 如果Cookie不爲空
for(Cookie cookie :request.getCookies()){ // 遍歷Cookie
if(cookie.getName().equals("account")) // 如果Cookie名爲
account
account = cookie.getValue(); // 保存account內容
if(cookie.getName().equals("ssid")) // 如果爲SSID
ssid = cookie.getValue(); // 保存SSID內容
}
}
if(account != null && ssid !=null){ // 如果account、SSID都不爲空
login =ssid.equals(calcMD5(account + KEY));
// 如果加密規則正確, 則視爲已經登錄
}
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01Transitional//EN">
<legend><%= login ? "歡迎您回來" : "請先登錄" %></legend>
<% if(login){%>
歡迎您, ${ cookie.account.value }.
<a href="${ pageContext.request.requestURI }?action=logout">
註銷</a>
<% } else {%>
<formaction="${ pageContext.request.requestURI }?action=login"
method="post">
<table>
<tr><td>賬號: </td>
<td><input type="text"name="account" style="width:
200px; "></td>
</tr>
<tr><td>密碼: </td>
<td><inputtype="password" name="password"></td>
</tr>
<tr>
<td>有效期: </td>
<td><inputtype="radio" name="timeout" value="-1"
checked> 關閉瀏覽器即失效 <br/> <input type="radio"
name="timeout" value="<%= 30 *24 * 60 * 60 %>"> 30天
內有效 <br/> <input type="radio" name="timeout"value=
"<%= Integer.MAX_VALUE %>"> 永久有效 <br/> </td> </tr>
<tr><td></td>
<td><inputtype="submit" value=" 登 錄 " class=
"button"></td>
</tr>
</table>
</form>
<% } %>
登錄時可以選擇登錄信息的有效期:關閉瀏覽器即失效、30天內有效與永久有效。通過設置Cookie的age屬性來實現,注意觀察代碼。運行效果如圖5.7所示。
圖5.7 永久登錄
%提示:該加密機制中最重要的部分爲算法與密鑰。由於MD5算法的不可逆性,即使用戶知道了賬號與加密後的字符串,也不可能解密得到密鑰。因此,只要保管好密鑰與算法,該機制就是安全的。
5.2 Session機制
除了使用Cookie,Web應用程序中還經常使用Session來記錄客戶端狀態。Session是服務器端使用的一種記錄客戶端狀態的機制,使用上比Cookie簡單一些,相應的也增加了服務器的存儲壓力。
5.2.1 什麼是Session
Session是另一種記錄客戶狀態的機制,不同的是Cookie保存在客戶端瀏覽器中,而Session保存在服務器上。客戶端瀏覽器訪問服務器的時候,服務器把客戶端信息以某種形式記錄在服務器上。這就是Session。客戶端瀏覽器再次訪問時只需要從該Session中查找該客戶的狀態就可以了。
如果說Cookie機制是通過檢查客戶身上的“通行證”來確定客戶身份的話,那麼Session機制就是通過檢查服務器上的“客戶明細表”來確認客戶身份。Session相當於程序在服務器上建立的一份客戶檔案,客戶來訪的時候只需要查詢客戶檔案表就可以了。
5.2.2 實現用戶登錄
Session對應的類爲javax.servlet.http.HttpSession類。每個來訪者對應一個Session對象,所有該客戶的狀態信息都保存在這個Session對象裏。Session對象是在客戶端第一次請求服務器的時候創建的。Session也是一種key-value的屬性對,通過getAttribute(Stringkey)和setAttribute(String key,Object value)方法讀寫客戶狀態信息。Servlet裏通過request.getSession()方法獲取該客戶的Session,例如:
HttpSession session = request.getSession(); // 獲取Session對象
session.setAttribute("loginTime", newDate()); // 設置Session中的屬性
out.println("登錄時間爲:" +(Date)session.getAttribute("loginTime"));
// 獲取Session屬性
request還可以使用getSession(booleancreate)來獲取Session。區別是如果該客戶的Session不存在,request.getSession()方法會返回null,而getSession(true)會先創建Session再將Session返回。
Servlet中必須使用request來編程式獲取HttpSession對象,而JSP中內置了Session隱藏對象,可以直接使用。如果使用聲明瞭<%@page session="false" %>,則Session隱藏對象不可用。下面的例子使用Session記錄客戶賬號信息。源代碼如下:
代碼5.9 session.jsp
<%@ page language="java"pageEncoding="UTF-8"%>
<jsp:directive.pageimport="com.helloweenvsfei.sessionWeb.bean.Person"/>
<jsp:directive.pageimport="java.text.SimpleDateFormat"/>
<jsp:directive.pageimport="java.text.DateFormat"/>
<jsp:directive.pageimport="java.util.Date"/>
<%!
DateFormat dateFormat = newSimpleDateFormat("yyyy-MM-dd");
// 日期格式化器
%>
<%
response.setCharacterEncoding("UTF-8"); // 設置request編碼
Person[] persons = { // 基礎數據,保存三個人的信息
newPerson("Liu Jinghua", "password1", 34, dateFormat.parse
("1982-01-01")),
newPerson("Hello Kitty", "hellokitty", 23, dateFormat.parse
("1984-02-25")),
new Person("Garfield","garfield_pass", 23, dateFormat.parse
("1994-09-12")),
};
String message =""; // 要顯示的消息
if(request.getMethod().equals("POST")){ // 如果是POST登錄
for(Person person :persons){ // 遍歷基礎數據,驗證賬號、密碼
// 如果 用戶名正確 且 密碼正確
if(person.getName().equalsIgnoreCase(request.getParameter
("username"))
&& person.getPassword().equals(request.getParameter
("password"))){
// 登錄成功,設置將用戶的信息以及登錄時間保存到 Session
session.setAttribute("person", person);// 保存登錄的Person
session.setAttribute("loginTime", new Date());
// 保存登錄的時間
response.sendRedirect(request.getContextPath() + "/
welcome.jsp");
return;
}
}
message = "用戶名密碼不匹配,登錄失敗。"; // 登錄失敗
}
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01Transitional//EN">
<html>
// ... HTML代碼爲一個FORM表單,代碼略,請看隨書光盤
</html>
登錄界面驗證用戶登錄信息,如果登錄正確,就把用戶信息以及登錄時間保存進Session,然後轉到歡迎頁面welcome.jsp。welcome.jsp中從Session中獲取信息,並將用戶資料顯示出來。welcome.jsp代碼如下:
代碼5.10 welcome.jsp
<%@ page language="java"pageEncoding="UTF-8"%>
<jsp:directive.pageimport="com.helloweenvsfei.sessionWeb.bean.Person"/>
<jsp:directive.pageimport="java.text.SimpleDateFormat"/>
<jsp:directive.page import="java.text.DateFormat"/>
<jsp:directive.pageimport="java.util.Date"/>
<%!
DateFormat dateFormat = newSimpleDateFormat("yyyy-MM-dd");
// 日期格式化器
%>
<%
Person person =(Person)session.getAttribute("person");
// 獲取登錄的person
Date loginTime =(Date)session.getAttribute("loginTime");
// 獲取登錄時間
%>
// ... 部分HTML代碼略
<table>
<tr><td>您的姓名: </td>
<td><%= person.getName()%></td>
</tr>
<tr><td>登錄時間: </td>
<td><%= loginTime%></td>
</tr>
<tr><td>您的年齡: </td>
<td><%= person.getAge()%></td>
</tr>
<tr><td>您的生日: </td>
<td><%=dateFormat.format(person.getBirthday()) %></td>
</tr>
</table>
程序運行效果如圖5.8所示。
圖5.8 使用Session記錄用戶信息
程序中使用了一個JavaBean類Person,源代碼請參看隨書光盤。注意程序中Session中直接保存了Person類對象與Date類對象,使用起來要比Cookie方便。
當多個客戶端執行程序時,服務器會保存多個客戶端的Session。獲取Session的時候也不需要聲明獲取誰的Session。Session機制決定了當前客戶只會獲取到自己的Session,而不會獲取到別人的Session。各客戶的Session也彼此獨立,互不可見。
%提示:Session的使用比Cookie方便,但是過多的Session存儲在服務器內存中,會對服務器造成壓力。
5.2.3 Session的生命週期
Session保存在服務器端。爲了獲得更高的存取速度,服務器一般把Session放在內存裏。每個用戶都會有一個獨立的Session。如果Session內容過於複雜,當大量客戶訪問服務器時可能會導致內存溢出。因此,Session裏的信息應該儘量精簡。
Session在用戶第一次訪問服務器的時候自動創建。需要注意只有訪問JSP、Servlet等程序時纔會創建Session,只訪問HTML、IMAGE等靜態資源並不會創建Session。如果尚未生成Session,也可以使用request.getSession(true)強制生成Session。
Session生成後,只要用戶繼續訪問,服務器就會更新Session的最後訪問時間,並維護該Session。用戶每訪問服務器一次,無論是否讀寫Session,服務器都認爲該用戶的Session“活躍(active)”了一次。
5.2.4 Session的有效期
由於會有越來越多的用戶訪問服務器,因此Session也會越來越多。爲防止內存溢出,服務器會把長時間內沒有活躍的Session從內存刪除。這個時間就是Session的超時時間。如果超過了超時時間沒訪問過服務器,Session就自動失效了。
Session的超時時間爲maxInactiveInterval屬性,可以通過對應的getMaxInactiveInterval()獲取,通過setMaxInactiveInterval(longinterval)修改。
Session的超時時間也可以在web.xml中修改。另外,通過調用Session的invalidate()方法可以使Session失效。
5.2.5 Session的常用方法
Session中包括各種方法,使用起來要比Cookie方便得多。Session的常用方法如表5.2所示。
表5.2 HttpSession的常用方法
方 法 名 |
描 述 |
void setAttribute(String attribute, Object value) |
設置Session屬性。value參數可以爲任何Java Object。通常爲Java Bean。value信息不宜過大 |
String getAttribute(String attribute) |
返回Session屬性 |
Enumeration getAttributeNames() |
返回Session中存在的屬性名 |
void removeAttribute(String attribute) |
移除Session屬性 |
String getId() |
返回Session的ID。該ID由服務器自動創建,不會重複 |
long getCreationTime() |
返回Session的創建日期。返回類型爲long,常被轉化爲Date類型,例如:Date createTime = new Date(session.get CreationTime()) |
long getLastAccessedTime() |
返回Session的最後活躍時間。返回類型爲long |
int getMaxInactiveInterval() |
返回Session的超時時間。單位爲秒。超過該時間沒有訪問,服務器認爲該Session失效 |
void setMaxInactiveInterval(int second) |
設置Session的超時時間。單位爲秒 |
void putValue(String attribute, Object value) |
不推薦的方法。已經被setAttribute(String attribute, Object Value)替代 |
Object getValue(String attribute) |
不被推薦的方法。已經被getAttribute(String attr)替代 |
boolean isNew() |
返回該Session是否是新創建的 |
void invalidate() |
使該Session失效 |
Tomcat中Session的默認超時時間爲20分鐘。通過setMaxInactiveInterval(intseconds)修改超時時間。可以修改web.xml改變Session的默認超時時間。例如修改爲60分鐘:
<session-config>
<session-timeout>60</session-timeout> <!-- 單位:分鐘 -->
</session-config>
%注意:<session-timeout>參數的單位爲分鐘,而setMaxInactiveInterval(int s)單位爲秒。
5.2.6 Session對瀏覽器的要求
雖然Session保存在服務器,對客戶端是透明的,它的正常運行仍然需要客戶端瀏覽器的支持。這是因爲Session需要使用Cookie作爲識別標誌。HTTP協議是無狀態的,Session不能依據HTTP連接來判斷是否爲同一客戶,因此服務器向客戶端瀏覽器發送一個名爲JSESSIONID的Cookie,它的值爲該Session的id(也就是HttpSession.getId()的返回值)。Session依據該Cookie來識別是否爲同一用戶。
該Cookie爲服務器自動生成的,它的maxAge屬性一般爲–1,表示僅當前瀏覽器內有效,並且各瀏覽器窗口間不共享,關閉瀏覽器就會失效。因此同一機器的兩個瀏覽器窗口訪問服務器時,會生成兩個不同的Session。但是由瀏覽器窗口內的鏈接、腳本等打開的新窗口(也就是說不是雙擊桌面瀏覽器圖標等打開的窗口)除外。這類子窗口會共享父窗口的Cookie,因此會共享一個Session。
%注意:新開的瀏覽器窗口會生成新的Session,但子窗口除外。子窗口會共用父窗口的Session。例如,在鏈接上右擊,在彈出的快捷菜單中選擇“在新窗口中打開”時,子窗口便可以訪問父窗口的Session。
如果客戶端瀏覽器將Cookie功能禁用,或者不支持Cookie怎麼辦?例如,絕大多數的手機瀏覽器都不支持Cookie。JavaWeb提供了另一種解決方案:URL地址重寫。
5.2.7 URL地址重寫
URL地址重寫是對客戶端不支持Cookie的解決方案。URL地址重寫的原理是將該用戶Session的id信息重寫到URL地址中。服務器能夠解析重寫後的URL獲取Session的id。這樣即使客戶端不支持Cookie,也可以使用Session來記錄用戶狀態。HttpServletResponse類提供了encodeURL(Stringurl)實現URL地址重寫,例如:
<td>
<a href="<%=response.encodeURL("index.jsp?c=1&wd=Java") %>">
Homepage</a>
</td>
該方法會自動判斷客戶端是否支持Cookie。如果客戶端支持Cookie,會將URL原封不動地輸出來。如果客戶端不支持Cookie,則會將用戶Session的id重寫到URL中。重寫後的輸出可能是這樣的:
<td>
<ahref="index.jsp;jsessionid=0CCD096E7F8D97B0BE608AFDC3E1931E?c=
1&wd=Java">Homepage</a>
</td>
即在文件名的後面,在URL參數的前面添加了字符串“;jsessionid=XXX”。其中XXX爲Session的id。分析一下可以知道,增添的jsessionid字符串既不會影響請求的文件名,也不會影響提交的地址欄參數。用戶單擊這個鏈接的時候會把Session的id通過URL提交到服務器上,服務器通過解析URL地址獲得Session的id。
如果是頁面重定向(Redirection),URL地址重寫可以這樣寫:
<%
if(“administrator”.equals(userName)){
response.sendRedirect(response.encodeRedirectURL(“administrator.jsp”));
return;
}
%>
效果跟response.encodeURL(Stringurl)是一樣的:如果客戶端支持Cookie,生成原URL地址,如果不支持Cookie,傳回重寫後的帶有jsessionid字符串的地址。
對於WAP程序,由於大部分的手機瀏覽器都不支持Cookie,WAP程序都會採用URL地址重寫來跟蹤用戶會話。比如用友集團的移動商街等。
%注意:TOMCAT判斷客戶端瀏覽器是否支持Cookie的依據是請求中是否含有Cookie。儘管客戶端可能會支持Cookie,但是由於第一次請求時不會攜帶任何Cookie(因爲並無任何Cookie可以攜帶),URL地址重寫後的地址中仍然會帶有jsessionid。當第二次訪問時服務器已經在瀏覽器中寫入Cookie了,因此URL地址重寫後的地址中就不會帶有jsessionid了。
5.2.8 Session中禁止使用Cookie
既然WAP上大部分的客戶瀏覽器都不支持Cookie,索性禁止Session使用Cookie,統一使用URL地址重寫會更好一些。JavaWeb規範支持通過配置的方式禁用Cookie。下面舉例說一下怎樣通過配置禁止使用Cookie。
打開項目sessionWeb的WebRoot目錄下的META-INF文件夾(跟WEB-INF文件夾同級,如果沒有則創建),打開context.xml(如果沒有則創建),編輯內容如下:
代碼5.11 /META-INF/context.xml
<?xml version='1.0' encoding='UTF-8'?>
<Context path="/sessionWeb"cookies="false">
</Context>
或者修改Tomcat全局的conf/context.xml,修改內容如下:
代碼5.12 context.xml
<!-- The contents of this file will be loaded for eachweb application -->
<Context cookies="false">
<!-- ... 中間代碼略 -->
</Context>
部署後TOMCAT便不會自動生成名JSESSIONID的Cookie,Session也不會以Cookie爲識別標誌,而僅僅以重寫後的URL地址爲識別標誌了。
%注意:該配置只是禁止Session使用Cookie作爲識別標誌,並不能阻止其他的Cookie讀寫。也就是說服務器不會自動維護名爲JSESSIONID的Cookie了,但是程序中仍然可以讀寫其他的Cookie。
5.3 Session與Cookie的比較
Cookie與Session都可以進行會話跟蹤,但是實現的原理不太一樣。一般情況下二者均可以滿足需求,但有時候不可以使用Cookie,有時候不可以使用Session。下面通過比較說明二者的特點以及適用的場合。
5.3.1 從存取方式上比較
Cookie中只能保存ASCII字符串,如果需要存取Unicode字符或者二進制數據,需要進行UTF-8,GBK或者BASE64等方式的編碼。Cookie中也不能直接存取Java對象。若要存儲稍微複雜的信息,使用Cookie是比較困難的。
而Session中可以存取任何類型的數據,包括而不限於String、Integer、List、Map等。Session中也可以直接保存Java Bean乃至任何Java類,對象等,使用起來非常方便。可以把Session看做是一個Java容器類。
5.3.2 從隱私安全上比較
Cookie存儲在客戶端瀏覽器中,對客戶端是可見的,客戶端的一些程序可能會窺探、複製甚至修改Cookie中的內容。而Session存儲在服務器上,對客戶端是透明的,不存在敏感信息泄露的危險。
如果選用Cookie,比較好的辦法是,敏感的信息如賬號密碼等儘量不要寫到Cookie中。最好是像Google、Baidu那樣將Cookie信息加密,提交到服務器後再進行解密,保證Cookie中的信息只有自己能讀得懂。而如果選擇Session就省事多了,反正是放在服務器上,Session裏任何隱私都可以。
5.3.3 從有效期上比較
使用過Google的人都知道,如果登錄過Google,則Google的登錄消息長期有效。用戶不必每次訪問都重新登錄,Google會長久地記錄該用戶的登錄信息。要達到這種效果,使用Cookie會是比較好的選擇。只需要設置Cookie的maxAge屬性爲一個很大很大的數字或者Integer.MAX_VALUE就可以了。Cookie的maxAge屬性支持這樣的效果。
使用Session理論上也能實現這種效果。只要調用方法setMaxInactiveInterval(Integer.MAX_VALUE)不就可以了麼。但是由於Session依賴於名爲JSESSIONID的Cookie,而Cookie JSESSIONID的maxAge默認爲–1,只要關閉了瀏覽器該Session就會失效,因此Session不能實現信息永久有效的效果。使用URL地址重寫也不能實現。
而且如果設置Session的超時時間過長,服務器累計的Session就會越多,越容易導致內存溢出。
5.3.4 從對服務器的負擔上比較
Session是保存在服務器端的,每個用戶都會產生一個Session。如果併發訪問的用戶非常多,會產生非常多的Session,消耗大量的內存。因此像Google、Baidu、Sina這樣併發訪問量極高的網站,是不太可能使用Session來追蹤客戶會話的。
而Cookie保存在客戶端,不佔用服務器資源。如果併發瀏覽的用戶非常多,Cookie是很好的選擇。對於Google、Baidu、Sina來說,Cookie也許是唯一的選擇。
5.3.5 從瀏覽器支持上比較
Cookie是需要客戶端瀏覽器支持的。如果客戶端禁用了Cookie,或者不支持Cookie,則會話跟蹤會失效。對於WAP上的應用,常規的Cookie就派不上用場了。
如果客戶端瀏覽器不支持Cookie,需要使用Session以及URL地址重寫。需要注意的是所有的用到Session程序的URL都要使用response.encodeURL(StringURL)或者response.encodeRedirectURL(StringURL)進行URL地址重寫,否則導致Session會話跟蹤失敗。對於WAP應用來說,Session+URL地址重寫也許是它唯一的選擇。
如果客戶端支持Cookie,則Cookie既可以設爲本瀏覽器窗口以及子窗口內有效(把maxAge設爲–1),也可以設爲所有瀏覽器窗口內有效(把maxAge設爲某個大於0的整數)。但Session只能在本瀏覽器窗口以及其子窗口內有效。如果兩個瀏覽器窗口互不相干,它們將使用兩個不同的Session。
5.3.6 從跨域名上比較
Cookie支持跨域名訪問,例如將domain屬性設置爲“.helloweenvsfei.com”,則以“.helloweenvsfei.com”爲後綴的所有域名均可以訪問該Cookie。跨域名Cookie現在被廣泛用在網絡中,例如Google、Baidu、Sina等。而Session則不會支持跨域名訪問。Session僅在他所在的域名內有效。
%注意:僅使用Cookie或者僅使用Session可能實現不了理想的效果。這時應該嘗試一下同時使用Cookie與Session。Cookie與Session的搭配使用在實際項目中會實現絢爛多姿的效果。
5.4 本 章 小 結
Cookie是早期的會話跟蹤技術,它將信息保存到客戶端瀏覽器中。瀏覽器訪問網站時會攜帶這些Cookie信息,達到鑑別身份的目的。
Session是在Cookie基礎上建立的會話跟蹤技術,它將信息保存在服務器端,Session中能夠存儲負責的Java對象,因此使用更加方便。Session依賴於名爲JSESSIONID的Cookie。
如果客戶端瀏覽器不支持Cookie,或者禁用了Cookie,仍然可以通過使用URL地址重寫來使用Session。