一、什么是session?
最近在學習node.js 的express框架,接觸到了關於session方面的內容。翻閱了一些的博客,學到了不少東西,發現一篇博文講的很好,概念內容摘抄如下:
Session是什么 Session一般譯作會話,牛津詞典對其的解釋是進行某活動連續的一段時間。從不同的層面看待session,它有着類似但不全然相同的含義。比如,在web應用的用戶看來,他打開瀏覽器訪問一個電子商務網站,登錄、並完成購物直到關閉瀏覽器,這是一個會話。而在web應用的開發者開來,用戶登錄時我需要創建一個數據結構以存儲用戶的登錄信息,這個結構也叫做session。因此在談論session的時候要注意上下文環境。而本文談論的是一種基於HTTP協議的用以增強web應用能力的機制或者說一種方案,它不是單指某種特定的動態頁面技術,而這種能力就是保持狀態,也可以稱作保持會話。 為什么需要session 談及session一般是在web應用的背景之下,我們知道web應用是基於HTTP協議的,而HTTP協議恰恰是一種無狀態協議。也就是說,用戶從A頁面跳轉到B頁面會重新發送一次HTTP請求,而服務端在返回響應的時候是無法獲知該用戶在請求B頁面之前做了什么的。 對於HTTP的無狀態性的原因,相關RFC里並沒有解釋,但聯系到HTTP的歷史以及應用場景,我們可以推測出一些理由: 1. 設計HTTP最初的目的是為了提供一種發布和接收HTML頁面的方法。那個時候沒有動態頁面技術,只有純粹的靜態HTML頁面,因此根本不需要協議能保持狀態; 2. 用戶在收到響應時,往往要花一些時間來閱讀頁面,因此如果保持客戶端和服務端之間的連接,那么這個連接在大多數的時間里都將是空閑的,這是一種資源的無端浪費。所以HTTP原始的設計是默認短連接,即客戶端和服務端完成一次請求和響應之后就斷開TCP連接,服務器因此無法預知客戶端的下一個動作,它甚至都不知道這個用戶會不會再次訪問,因此讓HTTP協議來維護用戶的訪問狀態也全然沒有必要; 3. 將一部分復雜性轉嫁到以HTTP協議為基礎的技術之上可以使得HTTP在協議這個層面上顯得相對簡單,而這種簡單也賦予了HTTP更強的擴展能力。事實上,session技術從本質上來講也是對HTTP協議的一種擴展。 總而言之,HTTP的無狀態是由其歷史使命而決定的。但隨着網絡技術的蓬勃發展,人們再也不滿足於死板乏味的靜態HTML,他們希望web應用能動起來,於是客戶端出現了腳本和DOM技術,HTML里增加了表單,而服務端出現了CGI等等動態技術。 而正是這種web動態化的需求,給HTTP協議提出了一個難題:一個無狀態的協議怎樣才能關聯兩次連續的請求呢?也就是說無狀態的協議怎樣才能滿足有狀態的需求呢? 此時有狀態是必然趨勢而協議的無狀態性也是木已成舟,因此我們需要一些方案來解決這個矛盾,來保持HTTP連接狀態,於是出現了cookie和session。 對於此部分內容,讀者或許會有一些疑問,筆者在此先談兩點: 1. 無狀態性和長連接 可能有人會問,現在被廣泛使用的HTTP1.1默認使用長連接,它還是無狀態的嗎? 連接方式和有無狀態是完全沒有關系的兩回事。因為狀態從某種意義上來講就是數據,而連接方式只是決定了數據的傳輸方式,而不能決定數據。長連接是隨着計算機性能的提高和網絡環境的改善所采取的一種合理的性能上的優化,一般情況下,web服務器會對長連接的數量進行限制,以免資源的過度消耗。 2. 無狀態性和session Session是有狀態的,而HTTP協議是無狀態的,二者是否矛盾呢? Session和HTTP協議屬於不同層面的事物,后者屬於ISO七層模型的最高層應用層,前者不屬於后者,前者是具體的動態頁面技術來實現的,但同時它又是基於后者的。在下文中筆者會分析Servlet/Jsp技術中的session機制,這會使你對此有更深刻的理解。
Cookie和Session 上面提到解決HTTP協議自身無狀態的方式有cookie和session。二者都能記錄狀態,前者是將狀態數據保存在客戶端,后者則保存在服務端。 首先看一下cookie的工作原理,這需要有基本的HTTP協議基礎。 cookie是在RFC2109(已廢棄,被RFC2965取代)里初次被描述的,每個客戶端最多保持三百個cookie,每個域名下最多20個Cookie(實際上一般瀏覽器現在都比這個多,如Firefox是50個),而每個cookie的大小為最多4K,不過不同的瀏覽器都有各自的實現。對於cookie的使用,最重要的就是要控制cookie的大小,不要放入無用的信息,也不要放入過多信息。 無論使用何種服務端技術,只要發送回的HTTP響應中包含如下形式的頭,則視為服務器要求設置一個cookie: Set-cookie:name=name;expires=date;path=path;domain=domain 支持cookie的瀏覽器都會對此作出反應,即創建cookie文件並保存(也可能是內存cookie),用戶以后在每次發出請求時,瀏覽器都要判斷當前所有的cookie中有沒有沒失效(根據expires屬性判斷)並且匹配了path屬性的cookie信息,如果有的話,會以下面的形式加入到請求頭中發回服務端: Cookie: name="zj"; Path="/linkage" 服務端的動態腳本會對其進行分析,並做出相應的處理,當然也可以選擇直接忽略。 這里牽扯到一個規范(或協議)與實現的問題,簡單來講就是規范規定了做成什么樣子,那么實現就必須依據規范來做,這樣才能互相兼容,但是各個實現所使用的方式卻不受約束,也可以在實現了規范的基礎上超出規范,這就稱之為擴展了。無論哪種瀏覽器,只要想提供cookie的功能,那就必須依照相應的RFC規范來實現。所以這里服務器只管發Set-cookie頭域,這也是HTTP協議無狀態性的一種體現。 需要注意的是,出於安全性的考慮,cookie可以被瀏覽器禁用。 再看一下session的原理: 筆者沒有找到相關的RFC,因為session本就不是協議層面的事物。它的基本原理是服務端為每一個session維護一份會話信息數據,而客戶端和服務端依靠一個全局唯一的標識來訪問會話信息數據。用戶訪問web應用時,服務端程序決定何時創建session,創建session可以概括為三個步驟: 1. 生成全局唯一標識符(sessionid); 2. 開辟數據存儲空間。一般會在內存中創建相應的數據結構,但這種情況下,系統一旦掉電,所有的會話數據就會丟失,如果是電子商務網站,這種事故會造成嚴重的后果。不過也可以寫到文件里甚至存儲在數據庫中,這樣雖然會增加I/O開銷,但session可以實現某種程度的持久化,而且更有利於session的共享;
3. 將session的全局唯一標示符發送給客戶端。 問題的關鍵就在服務端如何發送這個session的唯一標識上。聯系到HTTP協議,數據無非可以放到請求行、頭域或Body里,基於此,一般來說會有兩種常用的方式:cookie和URL重寫。 1. Cookie 讀者應該想到了,對,服務端只要設置Set-cookie頭就可以將session的標識符傳送到客戶端,而客戶端此后的每一次請求都會帶上這個標識符,由於cookie可以設置失效時間,所以一般包含session信息的cookie會設置失效時間為0,即瀏覽器進程有效時間。至於瀏覽器怎么處理這個0,每個瀏覽器都有自己的方案,但差別都不會太大(一般體現在新建瀏覽器窗口的時候); 2. URL重寫 所謂URL重寫,顧名思義就是重寫URL。試想,在返回用戶請求的頁面之前,將頁面內所有的URL后面全部以get參數的方式加上session標識符(或者加在path info部分等等),這樣用戶在收到響應之后,無論點擊哪個鏈接或提交表單,都會在再帶上session的標識符,從而就實現了會話的保持。讀者可能會覺得這種做法比較麻煩,確實是這樣,但是,如果客戶端禁用了cookie的話,URL重寫將會是首選。 到這里,讀者應該明白我前面為什么說session也算作是對HTTP的一種擴展了吧。如下兩幅圖是筆者在Firefox的Firebug插件中的截圖,可以看到,當我第一次訪問index.jsp時,響應頭里包含了Set-cookie頭,而請求頭中沒有。當我再次刷新頁面時,圖二顯示在響應中不在有Set-cookie頭,而在請求頭中卻有了Cookie頭。注意一下Cookie的名字:jsessionid,顧名思義,就是session的標識符,另外可以看到兩幅圖中的jsessionid的值是相同的,原因筆者就不再多解釋了。另外讀者可能在一些網站上見過在最后附加了一段形如jsessionid=xxx的URL,這就是采用URL重寫來實現的session。 
(圖一,首次請求index.jsp)
(圖二,再次請求index.jsp) Cookie和session由於實現手段不同,因此也各有優缺點和各自的應用場景: 1. 應用場景 Cookie的典型應用場景是Remember Me服務,即用戶的賬戶信息通過cookie的形式保存在客戶端,當用戶再次請求匹配的URL的時候,賬戶信息會被傳送到服務端,交由相應的程序完成自動登錄等功能。當然也可以保存一些客戶端信息,比如頁面布局以及搜索歷史等等。 Session的典型應用場景是用戶登錄某網站之后,將其登錄信息放入session,在以后的每次請求中查詢相應的登錄信息以確保該用戶合法。當然還是有購物車等等經典場景; 2. 安全性 cookie將信息保存在客戶端,如果不進行加密的話,無疑會暴露一些隱私信息,安全性很差,一般情況下敏感信息是經過加密后存儲在cookie中,但很容易就會被竊取。而session只會將信息存儲在服務端,如果存儲在文件或數據庫中,也有被竊取的可能,只是可能性比cookie小了太多。 Session安全性方面比較突出的是存在會話劫持的問題,這是一種安全威脅,這在下文會進行更詳細的說明。總體來講,session的安全性要高於cookie; 3. 性能 Cookie存儲在客戶端,消耗的是客戶端的I/O和內存,而session存儲在服務端,消耗的是服務端的資源。但是session對服務器造成的壓力比較集中,而cookie很好地分散了資源消耗,就這點來說,cookie是要優於session的;
4. 時效性 Cookie可以通過設置有效期使其較長時間內存在於客戶端,而session一般只有比較短的有效期(用戶主動銷毀session或關閉瀏覽器后引發超時); 5. 其他 Cookie的處理在開發中沒有session方便。而且cookie在客戶端是有數量和大小的限制的,而session的大小卻只以硬件為限制,能存儲的數據無疑大了太多。
后文中我會主要針對express的session專門講解。主要參考的博客網址如下,並對博主的無私奉獻表示萬分感謝。
http://www.cnblogs.com/shoru/archive/2010/02/19/1669395.html (大話session)
http://blog.csdn.net/fangaoxin/article/details/6952954 (Cookie/Session機制詳解)
二、express框架之session 內存存儲
express-session 是基於express框專門用於處理session的中間件。這里不談express-session怎么安裝,只給出相應的實例代碼。另外,session的認證機制離不開cookie,需要同時使用cookieParser 中間件,有關的介紹可以專門參考https://github.com/expressjs/session/blob/master/README.md,或者參考http://blog.modulus.io/nodejs-and-express-sessions,這個博客上講的比較清楚。
1 var express = require('express'); 2 var session = require('express-session'); 3 var cookieParser = require('cookie-parser'); 4 5 var app = express(); 6 7 app.use(cookieParser()); 8 app.use(session({ 9 secret: '12345', 10 name: 'testapp', //這里的name值得是cookie的name,默認cookie的name是:connect.sid 11 cookie: {maxAge: 80000 }, //設置maxAge是80000ms,即80s后session和相應的cookie失效過期 12 resave: false, 13 saveUninitialized: true, 14 })); 15 16 17 app.get('/awesome', function(req, res){ 18 19 if(req.session.lastPage) { 20 console.log('Last page was: ' + req.session.lastPage + "."); 21 } 22 req.session.lastPage = '/awesome'; //每一次訪問時,session對象的lastPage會自動的保存或更新內存中的session中去。 23 res.send("You're Awesome. And the session expired time is: " + req.session.cookie.maxAge); 24 }); 25 26 app.get('/radical', function(req, res){ 27 if (req.session.lastPage) { 28 console.log('Last page was: ' + req.session.lastPage + "."); 29 } 30 req.session.lastPage = '/radical'; 31 res.send('What a radical visit! And the session expired time is: ' + req.session.cookie.maxAge); 32 }); 33 34 app.get('/tubular', function(req, res){ 35 if (req.session.lastPage){ 36 console.log("Last page was: " + req.session.lastPage + "."); 37 } 38 39 req.session.lastPage = '/tubular'; 40 res.send('Are you a suffer? And the session expired time is: ' + req.session.cookie.maxAge); 41 }); 42 43 44 app.listen(5000);
2.1 express-session中間件的使用:
只需要用express app的use方法將session掛載在‘/’路徑即可,這樣所有的路由都可以訪問到session。可以給要掛載的session傳遞不同的option參數,來控制session的不同特性。具體可以參見官網:https://github.com/expressjs/session/blob/master/README.md。
2.2 session內容的存儲和更改:
To store or access session data, simply use the request property req.session, which is (generally) serialized as JSON by the store, so nested objects are typically fine.
一旦我們將express-session中間件用use掛載后,我們可以很方便的通過req參數來存儲和訪問session對象的數據。req.session是一個JSON格式的JavaScript對象,我們可以在使用的過程中隨意的增加成員,這些成員會自動的被保存到option參數指定的地方,默認即為內存中去。
2.3 session的生命周期
session與發送到客戶端瀏覽器的生命周期是一致的。而我們在掛載session的時候,通過option選項的cookie.maxAge成員,我們可以設置session的過期時間,以ms為單位(但是,如果session存儲在mongodb中的話,任何低於60s(60000ms)的設置是沒有用的,下文會有詳細的解釋)。如果maxAge不設置,默認為null,這樣的expire的時間就是瀏覽器的關閉時間,即每次關閉瀏覽器的時候,session都會失效。
三、express框架之session 數據庫存儲
有時候,我們需要session的聲明周期要長一點,比如好多網站有個免密碼兩周內自動登錄的功能。基於這個需求,session必須尋找內存之外的存儲載體,數據庫能提供完美的解決方案。這里,我選用的是mongodb數據庫,作為一個NoSQL數據庫,它的基礎數據對象時database-collection-document 對象模型非常直觀並易於理解,針對node.js 也提供了豐富的驅動和API。express框架提供了針對mongodb的中間件:connect-mongo,我們只需在掛載session的時候在options中傳入mongodb的參數即可,程序運行的時候, express app 會自動的替我們管理session的存儲,更新和刪除。具體可以參考:
https://github.com/kcbanner/connect-mongo
測試代碼如下:
1 var express = require('express'); 2 var session = require('express-session'); 3 var cookieParser = require('cookie-parser'); 4 var MongoStore = require('connect-mongo')(session); 5 var app = express(); 6 7 app.use(cookieParser()); 8 app.use(session({ 9 secret: '12345', 10 name: 'testapp', 11 cookie: {maxAge: 80000 }, 12 resave: false, 13 saveUninitialized: true, 14 store: new MongoStore({ //創建新的mongodb數據庫 15 host: 'localhost', //數據庫的地址,本機的話就是127.0.0.1,也可以是網絡主機 16 port: 27017, //數據庫的端口號 17 db: 'test-app' //數據庫的名稱。 18 }) 19 })); 20 21 22 app.get('/awesome', function(req, res){ 23 24 if(req.session.lastPage) { 25 console.log('Last page was: ' + req.session.lastPage + "."); 26 } 27 req.session.lastPage = '/awesome'; 28 res.send("You're Awesome. And the session expired time is: " + req.session.cookie.maxAge); 29 }); 30 31 app.get('/radical', function(req, res){ 32 if (req.session.lastPage) { 33 console.log('Last page was: ' + req.session.lastPage + "."); 34 } 35 req.session.lastPage = '/radical'; 36 res.send('What a radical visit! And the session expired time is: ' + req.session.cookie.maxAge); 37 }); 38 39 app.get('/tubular', function(req, res){ 40 if (req.session.lastPage){ 41 console.log("Last page was: " + req.session.lastPage + "."); 42 } 43 44 req.session.lastPage = '/tubular'; 45 res.send('Are you a suffer? And the session expired time is: ' + req.session.cookie.maxAge); 46 }); 47 48 49 app.listen(5000);
跟session的內存存儲一樣,只需增加紅色部分的store選項即可,app會自動替我們把session存入到mongodb數據,而非內存中。
3.1 session的生命周期:
由於session是存在服務器端數據庫的,所以的它的生命周期可以持久化,而不僅限於瀏覽器關閉的時間。具體是由cookie.maxAge 決定:如果maxAge設定是1個小時,那么從這個因瀏覽器訪問服務器導致session創建開始后,session會一直保存在服務器端,即使瀏覽器關閉,session也會繼續存在。如果此時服務器宕機,只要開機后數據庫沒發生不可逆轉的破壞,maxAge時間沒過期,那么session是可以繼續保持的。
當maxAge時間過期后,session會自動的數據庫中移除,對應的還有瀏覽器的cookie。不過,由於connect-mongo的特殊機制(每1分鍾檢查一次過期session),session的移除可能在時間上會有一定的滯后。
connect-mongo uses MongoDB's TTL collection feature (2.2+) to have mongod automatically remove expired sessions. (mongod runs this check every minute.) Note: By connect/express's default, session cookies are set to expire when the user closes their browser (maxAge: null). In accordance with standard industry practices, connect-mongo will set these sessions to expire two weeks from their last 'set'. You can override this behavior by manually setting the maxAge for your cookies -- just keep in mind that any value less than 60 seconds is pointless, as mongod will only delete expired documents in a TTL collection every minute.
當然,由於cookie是由瀏覽器廠商實現的,cookie不具有跨瀏覽器的特性,例如,我用firefox瀏覽器在京東上購物時,勾選了2周內免密碼輸入,但是當我第一次用IE登陸京東時,同樣要重新輸入密碼。所以,這對服務器的同一個操作,不同的瀏覽器發起的請求,會產生不同的session-cookie。