會話(Session)追蹤是Web程式中常用的技術,用來追蹤使用者的整個會話。常用的會話追蹤技術是Cookie與Session。 Cookie透過在客戶端記錄資訊來確定使用者身分,Session透過在伺服器端記錄資訊來確定使用者身分。
本章將系統性地講述Cookie與Session機制,並比較說明何時不能用Cookie,什麼時候不能用Session。
1.1 Cookie機制
在程式中,會話追蹤是很重要的事情。理論上,一個使用者的所有請求操作都應該屬於同一個會話,而另一個使用者的所有請求操作則應該屬於另一個會話,而二者不能混淆。例如,用戶A在超市購買的任何商品都應該放在A的購物車內,不論是用戶A什麼時間購買的,這都是屬於同一個會話的,不能放入用戶B或用戶C的購物車內,這不屬於同一個會話。
而Web應用程式是使用HTTP協定傳輸資料的。 HTTP協定是無狀態的協定。一旦資料交換完畢,客戶端與伺服器端的連線就會關閉,再次交換資料需要建立新的連線。這就意味著伺服器無法從連線上追蹤會話。 即用戶A購買了一件商品放入購物車內,當再次購買商品時伺服器已經無法判斷該購買行為是屬於用戶A的會話還是用戶B的會話了。要追蹤該會話,必須引入一種機制。
Cookie就是這樣的一種機制。它可以彌補HTTP協定無狀態的不足。在Session出現之前,基本上所有的網站都採用Cookie來追蹤會話。
1.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的內容,如圖1.1所示。
圖1.1 Baidu網站所頒發的Cookie
圖1.1中彈出的對話方塊中所顯示的為Baidu網站的Cookie。其中第一行BAIDUID記錄的就是筆者的身分helloweenvsfei,只是Baidu使用特殊的方法將Cookie資訊加密了。
注意:Cookie功能需要瀏覽器的支援。
如果瀏覽器不支援Cookie(如大部分手機中的瀏覽器)或是停用Cookie了,Cookie功能就會失效。
不同的瀏覽器採用不同的方式儲存Cookie。
IE瀏覽器會在「C:\Documents and Settings\你的使用者名稱\Cookies」資料夾下以文字檔案形式儲存,一個文字檔案儲存一個Cookie。
1.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該類別。
1.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做類似的處理。
1.1.4 Unicode編碼:保存中文
中文與英文字符不同,中文屬於Unicode字符,在記憶體中佔4個字符,而英文屬於ASCII字符,記憶體中只佔2個位元組。 Cookie中使用Unicode字元時需要對Unicode字元進行編碼,否則會亂碼。
提示:Cookie中儲存中文只能編碼。一般使用UTF-8編碼即可。不建議使用GBK等中文編碼,因為瀏覽器不一定支持,而且JavaScript也不支援GBK編碼。
1.1.5 BASE64編碼:儲存二進位圖片
Cookie不僅可以使用ASCII字元與Unicode字符,還可以使用二進位數據。例如在Cookie中使用數位證書,提供安全度。使用二進位資料時也需要進行編碼。
%注意:本程式僅用於展示Cookie中可以儲存二進位內容,並不實用。由於瀏覽器每次請求伺服器都會攜帶Cookie,因此Cookie內容不宜過多,否則影響速度。 Cookie的內容應該少而精。
1.1.6 設定Cookie的所有屬性
除了name與value之外,Cookie還有其他幾個常用的屬性。每個屬性對應一個getter方法與一個setter方法。 Cookie類的所有屬性如表1.1所示。
表1.1 Cookie常用屬性
屬 性 名稱 |
Tor 述 |
#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。注意第一個字元必須為「.」 |
1.1.7 Cookie的有效期限
Cookie的maxAge決定著Cookie的有效期,單位為秒(Second )。 Cookie中透過getMaxAge()方法與setMaxAge(int maxAge)方法來讀寫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,且無法為負數#11205; // 必須執行這句話
response物件提供的Cookie操作方法只有一個新增操作add(Cookie cookie)。 要想修改Cookie只能使用一個同名的Cookie來覆寫原來的Cookie,達到修改的目的。刪除時只需要把maxAge修改為0即可。注意:從客戶端讀取Cookie時,包含maxAge在內的其他屬性都是不可讀的,也不會被提交。瀏覽器提交Cookie時只會提交name與value屬性。 maxAge屬性只被瀏覽器用來判斷Cookie是否過期。
1.1.8 Cookie的修改、刪除
Cookie不提供修改、刪除動作。如果要修改某個Cookie,只需要新建一個同名的Cookie,加入到response中覆寫原來的Cookie。
如果要刪除某個Cookie,只需要新建一個同名的Cookie,並將maxAge設為0,並加入到response中覆蓋原來的Cookie。注意是0而不是負數。負數代表其他的意義。讀者可以透過上例的程序進行驗證,設定不同的屬性。
注意:修改、刪除Cookie時,新建的Cookie除value、maxAge之外的所有屬性,例如name、path、domain等,都要與原Cookie完全一樣。否則,瀏覽器將視為兩個不同的Cookie不予覆蓋,導致修改、刪除失敗。
1.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屬性分別為兩個域名,輸出到客戶端。
1.1.10 Cookie的路徑
domain屬性決定執行存取Cookie的域名,而path屬性則決定允許存取Cookie的路徑(ContextPath)。例如,如果只允許/sessionWeb/下的程式使用Cookie,可以這麼寫:
Cookie cookie = new Cookie("time","20080808"); // 新建Cookie
cookie .setPath("/session/"); //設定路徑
// 輸出到客戶端設定為「/」時允許所有路徑使用Cookie 。 path屬性需要使用符號“/”結尾。 name相同但domain相同的兩個Cookie也是兩個不同的Cookie。1.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); // 設定安全屬性上( // 輸出至客戶端 #
提示:secure屬性並不會對Cookie內容加密,因而無法保證絕對的安全性。如果需要高安全性,需要在程式中對Cookie內容加密、解密,以防洩密。
1.1.12 JavaScript作業Cookie
Cookie是儲存在瀏覽器端的,因此瀏覽器具有操作Cookie的先決條件。瀏覽器可以使用腳本程式如JavaScript或VBScript等操作Cookie。這裡以JavaScript為例介紹常用的Cookie操作。例如下面的程式碼會輸出本頁面所有的Cookie。
<script>document.write(document.cookie);</script>
由於JavaScript能夠任意讀寫Cookie,有些好事者便想使用JavaScript程式去窺探用戶在其他網站的Cookie。不過這是徒勞無功的,W3C組織早就意識到JavaScript對Cookie的讀寫所帶來的安全隱患並加以防備了,W3C標準的瀏覽器會阻止JavaScript讀寫任何不屬於自己網站的Cookie。換句話說,A網站的JavaScript程式讀寫B網站的Cookie不會有任何結果。
1.1.13 案例:永久登入
如果使用者是在自己家的電腦上上網,登入時就可以記住他的登入訊息,下次造訪時不需要再次登錄,直接造訪即可。實作方法是把登入資訊如帳號、密碼等保存在Cookie中,並控制Cookie的有效期,下次造訪時再驗證Cookie中的登入資訊即可。
儲存登入資訊有多種方案。最直接的是把使用者名稱與密碼都保持到Cookie中,下次造訪時檢查Cookie中的使用者名稱與密碼,與資料庫比較。這是一種比較危險的選擇,一般不把密碼等重要資訊存到Cookie中。
還有一種方案是把密碼加密後儲存到Cookie中,下次造訪時解密並與資料庫比較。 這種方案略微安全一些。如果不希望儲存密碼,也可以把登入的時間戳記儲存到Cookie與資料庫中,到時才驗證使用者名稱與登入時間戳就可以了。
這幾種方案驗證帳號時都要查詢資料庫。
本範例將採用另一種方案,只在登入時查詢一次資料庫,以後存取驗證登入資訊時不再查詢資料庫。實作方式是把帳號依照一定的規則加密後,連同帳號一塊儲存到Cookie中。下次造訪時只需要判斷帳號的加密規則是否正確即可。 本例把帳號儲存到名為account的Cookie中,把帳號連同金鑰用MD1演算法加密後儲存到名為ssid的Cookie中。驗證時驗證Cookie中的帳號與金鑰加密後是否與Cookie中的ssid相等。相關程式碼如下:
程式碼1.8 loginCookie.jsp
<%@ page language="java"pageEncoding="UTF-8" isErrorPage="false" %>
<%! // JSP方法
private static final String KEY =":cookie@helloweenvsfei.com";
public final static String calcMD1(Stringss) { // MD1 加密演算法
String s = ss ==null ?"" : ss; // 若為null回復為空格
## char hexDigits[] = { '0','1', '2, '1', '2, '33' ', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
## try {
byte[] strTemp =s.getBytes(); MessageDigestmdTemp = MessageDigest.getInstance("MD1"); // 取得MD1
mdTemp.update(strTemp); ); // 加密上
int j =md.length; // 加密後的長度
char str[] = newchar[j
*2]; // 計數器k
for (int i = 0; i< j; i++) { str[k++] =hexDigits[byte0 > ;>> 4 & 0xf];
## str[k++] =hexDigits[byte0 & 0xf]; // 加密後字串 } catch (Exception e){return null; } }##%>
# }##%>
<%
# request.setCharacterEncoding("UTF-8"); // 設定request編碼
# response.set? # # String action =request.getParameter("action"); // 取得action參數
if("login".equals(action)){
# String account =request.getParameter("account");// 取得account參數
String password =request.getParameter("password");
// 取得password參數 int timeout = newInteger(request.getParameter("timeout"));
// 取得timeout參數
,
## String ssid =calc
String ssid =calc
.把帳號、金鑰使用MD1加密後保存
currentTimeMillis());
CookieaccountCookie = new Cookie("account", "");
// 新建Cookie,內容為空白
accountCookie.setMaxAge(0); 為0,刪除
Cookie ssidCookie =新 C ; //設定有效期限為0,刪除
response.addCookie(accountCookie); Cookie(ssidCookie); // 輸出至客戶端
//重新請求此頁面,參數中帶有時間戳,禁止瀏覽器快取頁面內容
response.sendRedirect(request.getRequestURI() + "?" + System.#oo. );
return;
# }
String ssid = null; // SSID 識別 // 若Cookie不為空 for (Cookie cookie :request.getCookies()){ // 遍歷Cookie
if(cookie.getName().equals("account")) // 若Cookie名稱為
account
# account = cookie.getValue(); #
if(cookie.getName().equals("ssid")) // 若為SSID } } if(account != null && ssid !=null){ // 如果account、SSID都不為空 + KEY));或無法登錄HTML PUBLIC "- //W3C//DTD HTML 4.01Transitional//EN">
註銷
"${ pageContext.request.requestURI }?action=login"
method="post">帳號: | ## 200px ; "> ; ; | ;有效: | > name="timeout" value="* 24 ##*> > 30天 內效> |
<%= person.getName()% > | <%= loginTime%> | <%= person.getAge()%> | 您的生日: | <%=dateFormat.format(oxy
|
程式運作效果如圖1.8所示。
#圖1.8 使用Session記錄使用者資訊
注意程式中Session中直接保存了Person類別物件與Date類別對象,使用起來比Cookie方便。
當多個客戶端執行程式時,伺服器會保存多個客戶端的Session。取得Session的時候也不需要聲明取得誰的Session。 Session機制決定了目前客戶只會取得自己的Session,而不會取得別人的Session。各客戶的Session也彼此獨立,互不可見。
提示:Session的使用比Cookie方便,但是過多的Session儲存在伺服器記憶體中,會對伺服器造成壓力。
1.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)」了一次。
1.2.4 Session的有效期限
由於會有越來越多的使用者存取伺服器,因此Session也會越來越多。 為防止記憶體溢出,伺服器會把長時間內沒有活躍的Session從記憶體中刪除。這個時間就是Session的超時時間。如果超過了超時時間沒訪問過伺服器,Session就自動失效了。
Session的逾時時間為maxInactiveInterval屬性,可以透過對應的getMaxInactiveInterval()取得,透過setMaxInactiveInterval(longinterval)修改。
Session的超時時間也可以在web.xml中修改。另外,透過呼叫Session的invalidate()方法可以使Session失效。
1.2.5 Session的常用方法
Session中包含各種方法,使用起來比Cookie方便得多。 Session的常用方法如表1.2所示。
表1.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屬性 |
傳回Session的ID。 ID由伺服器自動創建,不會重複 | |
傳回Session的建立日期。傳回類型為long,常轉換為Date類型,例如:Date createTime = new Date(session.get CreationTime()) | |
返回Session的最後活躍時間。傳回型別為long | |
傳回Session的逾時時間。單位為秒。超過該時間沒有訪問,伺服器認為該Session失效 | |
設定Session的超時時間。單位為秒 | |
不推薦的方法。已經被setAttribute(String attribute, Object Value)取代 | |
#不推薦的方法。已經被getAttribute(String attr)取代 | |
傳回該Session是否是新建立的 | |
#使該Session失效 |
|
以上是Cookie/Session機制介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!