express 框架之session
一、什麼是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。