簡介
-
HTTP是無狀態協議。當瀏覽器中加載頁面,然后轉到同一網站的另一頁面時,服務器和瀏覽器都沒有任何內在的方法可以認識到,這是同一瀏覽器訪問同一網站。換一種說法,Web工作的方式就是在每個HTTP請求中都要包含所有必要的信息,服務器才能滿足這個請求。
-
所以需要用某種辦法在HTTP上建立狀態,於是便有了cookie和會話。
關於cookie
-
cookie的想法很簡單:服務器發送一點信息,瀏覽器在一段可配置的時期內保存它。發送哪些信息確實是由服務器來決定:通常只是一個唯一ID號,標識特定瀏覽器,從而維持一個有狀態的假象。
-
cookie對用戶來說不是加密的;
-
服務器向客戶端發送的所有cookie都能被客戶端查看。
-
用戶對cookie有絕對的控制權,可以刪除或禁用cookie
-
一般的cookie可以被篡改
-
不管瀏覽器什么時候發起一個跟cookie關聯的請求,盲目地相信cookie中的內容,都有可能會受到攻擊。要確保cookie不被篡改,使用簽名cookie。
cookie可以用於攻擊
-
跨站腳本攻擊 (XSS)攻擊方式。XSS攻擊中有一種技術就涉及用惡意的JavaScript修改cookie中的內容。所以不要輕易相信返回到服務器的cookie內容。
-
用簽名cookie會有幫助(不管是用戶修改的還是惡意JavaScript修改的,這些篡改都會在簽名cookie中留下明顯的痕跡),並且還可以設定選項指明cookie只能由服務器修改。這些cookie的用途會受限,但它們肯定更安全。**
-
如果可以選擇,會話要優於cookie
-
大多數情況下,可以用會話維持狀態; 並且會話更容易,不用擔心會濫用用戶的存儲,而且也更安全。
-
當服務器希望客戶端保存一個cookie時,它會發送一個響應頭
Set-Cookie
,其中包含名稱/值對。當客戶端向服務器發送含有cookie的請求時,它會發送多個請求頭Cookie,其中包含這些cookie的值。**
憑證的外化###
-
為了保證cookie的安全,必須有一個cookie秘鑰。cookie秘鑰是一個字符串,服務器知道它是什么,它會在cookie發送到客戶端之前對cookie加密。這是一個不需要記住的密碼,所以可以是隨機字符串。
-
推薦用一個隨機密碼生成器來生成cookie秘鑰。**
-
外化第三方憑證是一種常見的做法,比如cookie秘鑰、數據庫密碼和API令牌(Twitter、Facebook等)。這不僅易於維護(容易找到和更新憑證),還可以讓版本控制系統忽略這些憑證文件。這對放在GitHub或其他開源源碼控制庫上的開源代碼庫尤其重要。**
-
因此可以准備將憑證外化在一個JavaScript文件中(用JSON或XML也行,但我覺得JavaScript最容易)。
-
創建文件credentials.js:
module.exports = {
cookieSecret: '把你的cookie秘鑰放在這里',
};
- 現在,為了防止不慎把這個文件添加到源碼庫中,在.gitignore文件中加上credentials.js。
var credentials = require('./credentials.js');
Express中的Cookie###
在程序中開始設置和訪問cookie之前,需要先引入中間件cookie-parser
。首先npm install --save cookie-parser
,然后
app.use(require('cookie-parser')(credentials.cookieSecret));
- 完成這個之后,就可以在任何能訪問到響應對象的地方設置cookie或簽名cookie:
res.cookie('monster', 'nom nom');
res.cookie('signed_monster', 'nom nom', { signed: true });
-
簽名cookie的優先級高於未簽名cookie。如果將簽名cookie命名為
signed_monster
,那就不能用這個名字再命名未簽名cookie(它返回時會變成undefined)。 -
要獲取客戶端發送過來的cookie的值(如果有的話),只需訪問請求對象的cookie或
signedCookie
屬性:
var monster = req.cookies.monster;
var signedMonster = req.signedCookies.monster;
-
任何字符串都可以作為cookie的名稱。
-
要刪除cookie,用
res.clearCookie
:
res.clearCookie('monster');
設置cookie時可以使用如下這些選項:
domain
控制跟cookie關聯的域名。這樣可以將cookie分配給特定的子域名。注意,不能給cookie設置跟服務器所用域名不同的域名,因為那樣它什么也不會做。
path
控制應用這個cookie的路徑。注意,路徑會隱含地通配其后的路徑。如果用的路徑是/ (默認值),它會應用到網站的所有頁面上。如果用的路徑是/foo,它會應用到/foo、/foo/bar等路徑上。
maxAge
指定客戶端應該保存cookie多長時間,單位是毫秒。如果你省略了這一選項,瀏覽器關閉時cookie就會被刪掉。(也可以用expiration指定cookie過期的日期,但語法很麻煩。建議用maxAge。)
secure
指定該cookie只通過安全(HTTPS)連接發送。
httpOnly
將這個選項設為true表明這個cookie只能由服務器修改。也就是說客戶端JavaScript不能修改它。這有助於防范XSS攻擊。
signed
設為true會對這個cookie簽名,這樣就需要用res.signedCookies
而不是res.cookies
訪問它。被篡改的簽名cookie會被服務器拒絕,並且cookie值會重置為它的原始值。
會話###
-
會話實際上只是更方便的狀態維護方法。
-
要實現會話,必須在客戶端存些東西,否則服務器無法從一個請求到下一個請求中識別客戶端。
-
通常的做法是用一個包含唯一標識的cookie,然后服務器用這個標識獲取相應的會話信息; cookie不是實現這個目的的唯一手段,比如在URL中添加會話信息等;但這些技術混亂、困難且效率低下,所以最好別用。
-
HTML5為會話提供了另一種選擇,那就是本地存儲,但現在還沒有令人嘆服的理由去采用這種技術而放棄經過驗證有效的cookie。
-
從廣義上來說,有兩種實現會話的方法:
- 把所有東西都存在cookie里,被稱為“基於cookie的會話”,並且僅僅表示比使用cookie便利;然而,它還意味着要把添加到cookie中的所有東西都存在客戶端瀏覽器中,所以不推薦
- 只在cookie里存一個唯一標識,其他東西都存在服務器上。
內存存儲###
-
把會話信息存在服務器上,那么必須找個地方存儲它。入門級的選擇是內存會話。
-
它們非常容易設置,但也有個巨大的缺陷:重啟服務器后會話信息就消失了。更糟的是,如果擴展了多台服務器,那么每次請求可能是由不同的服務器處理的,所以會話數據有時在那里,有時不在。這明顯是不可接受的用戶體驗。
-
然而出於開發和測試的需要,有它就足夠了。
-
首先安裝express-session(
npm install --save express-sessio
n) -
然后,在鏈入cookie-parser之后鏈入express-session:
app.use(require('cookie-parser')(credentials.cookieSecret));
app.use(require('express-session')());
中間件express-session接受帶有如下選項的配置對象:
key
存放唯一會話標識的cookie名稱。默認為connect.sid
。
store
會話存儲的實例。默認為一個MemoryStore的實例,可以滿足我們當前的要求。
cookie
會話cookie的cookie設置 (path
、domain
、secure
等)。適用於常規的cookie默認值。
使用會話###
- 會話設置好以后,使用起來就再簡單不過了,只是使用請求對象的session變量的屬性:
req.session.userName = 'Anonymous';
var colorScheme = req.session.colorScheme || 'dark';
- 注意,對於會話而言,我們不是用請求對象獲取值,用響應對象設置值,它全都是在請求對象上操作的。(響應對象沒有session屬性。)要刪除會話,可以用JavaScript的
delete
操作符:
req.session.userName = null; // 這會將'userName'設為null;但不會移除它
delete req.session.colorScheme; // 這會移除'colorScheme'
用會話實現即顯消息###
- “即顯”消息(不要跟Adobe Flash搞混了)只是在不破壞用戶導航的前提下向用戶提供反饋的一種辦法。
- 用會話實現即顯消息是最簡單的方式(也可以用查詢字符串,但那樣除了URL會更丑外,還會把即顯消息放到書簽里)
//使用了bootstrap
{{#if flash}}
<div class="alert alert-dismissible alert-{{flash.type}}">
<button type="button" class="close"
data-dismiss="alert" aria-hidden="true">×</button>
<strong>{{flash.intro}}</strong> {{{flash.message}}}
</div>
{{/if}}
- 注意,在flash.message外面用了3個大括號,這樣我們就可以在消息中使用簡單的HTML(可能是要加重單詞或包含超鏈接)。
- 接下來添加一些中間件,如果會話中有flash對象,將它添加到上下文中。即顯消息顯示過一次之后,我們就要從會話中去掉它,以免它在下一次請求時再次顯示。在路由之前添加下面這段代碼:**
app.use(function(req, res, next){
// 如果有即顯消息,把它傳到上下文中,然后清除它
res.locals.flash = req.session.flash;
delete req.session.flash;
next();
});
接下來我們看一下如何使用即顯消息。假設我們的用戶訂閱了簡報,並且我們想在用戶訂閱后把他們重定向到簡報歸檔頁面去。我們的表單處理器可能是這樣的:
app.post('/newsletter', function(req, res){
var name = req.body.name || '', email = req.body.email || '';
// 輸入驗證
if(!email.match(VALID_EMAIL_REGEX)) {
if(req.xhr) return res.json({ error: 'Invalid name email address.' });
req.session.flash = {
type: 'danger',
intro: 'Validation error!',
message: 'The email address you entered was not valid.',
};
return res.redirect(303, '/newsletter/archive');
}
new NewsletterSignup({ name: name, email: email }).save(function(err){
if(err) {
if(req.xhr) return res.json({ error: 'Database error.' });
req.session.flash = {
type: 'danger',
intro: 'Database error!',
message: 'There was a database error; please try again later.',
}
return res.redirect(303, '/newsletter/archive');
}
if(req.xhr) return res.json({ success: true });
req.session.flash = {
type: 'success',
intro: 'Thank you!',
message: 'You have now been signed up for the newsletter.',
};
return res.redirect(303, '/newsletter/archive');
});
});
看如何用同一個處理器處理AJAX提交(因為我們檢查了req.xhr),並且區分開了輸入驗證錯誤和數據庫錯誤。記住,即便在前端做了輸入驗證,在后台也應該再做一次。
即顯消息是網站中一種很棒的機制,即便在某些特定區域其他方法更合適一些(比如,即顯消息在多表單“向導”或購物車結賬流程中就不太合適)。
因為在中間件里把即顯消息從會話中傳給了res.locals.flash
,所以必須執行重定向以便顯示即顯消息。如果不想通過重定向顯示即顯消息,直接設定res.locals.flash
,而不是req.session.flash
。
會話的用途###
- 當想跨頁保存用戶的偏好時,可以用會話。會話最常見的用法是提供用戶驗證信息,在登錄后就會創建一個會話。之后就不用在每次重新加載頁面時再登錄一次。
- 即便沒有用戶賬號,會話也有用。網站一般都要記住你喜歡如何排列東西,或者你喜歡哪種日期格式,這些都不需要登錄。
- 盡管我建議優先選擇會話而不是cookie,但理解cookie的工作機制也很重要(特別是因為有cookie才能用會話)。它對於你在應用中診斷問題、理解安全性及隱私問題都有幫助。