這幾年 SPA(Single Page App)當道,造就一個現象就是 Server-side 及 Client-side 壁壘分明。如果你是 Client-side 的開發者,可能沒什麼機會自己架一個 web server,所以也不會實作到 session。自己本身也因為這樣,雖然開發 Web 應用已經有幾年的時間,汗顏的是,一直沒有好好了解 session 及 cookie。
前一個專案還是使用 SPA 架構,但上線後發現,SPA 要 tune SEO 相當的麻煩,這是從架構面就有的問題,因此最後是用相當骯髒的方式來處理。新的專案想要避免這種情況,決定自己架 web server 來提供靜態頁面。
啊有點離題了,希望未來有機會能談談前述的主題,但總之,這樣的架構(web server 提供靜態頁面),client 端對 api server 的驗證還是靠 access token,但如果是對 web server 本身的話就可以利用 session 來驗證使用者。
Node.js 上面有一個好用的驗證身分 library — passport.js。當然,照著官方教學你可以輕易的在你的 web app 上作單純的帳密登入甚至是第三方登入,乍看之下很神,但如果你了解 session 和 cookie 的機制的話,你會發現這不是無中生有,都是有跡可循的。
先從 cookie 開始介紹!
Cookie
最常見到的 Cookie 應用是在表單填寫:假設現在頁面上的資料填到一半,不小心把網頁關掉,這時再重新打開發現先前填的內容還在的話,靠的就是 cookie。
實作原理很簡單,client 端的程式在一旦填寫的資料有變動時,就把該資訊寫入 cookie。Cookie 由瀏覽器處理,具有兩個特性:
-
Domain specific:只針對原本的 domain 起作用。舉例,在
*.example.com
存入的 cookie,不會出現在*.not-example.com
。 -
到了所設定的生命期限之後會失效。(如果是在 server-side 設定的話,預設是在關閉瀏覽器後失效,後面會詳述)。
因為第二點,所以在新增 cookie 時,要指定 key 及 value,還有生命期限:
// 在 www.example.com 的頁面中
function setCookie(cname, cvalue, exdays) {
var d = new Date();
d.setTime(d.getTime() + (exdays*24*60*60*1000));
var expires = "expires="+d.toUTCString();
document.cookie = cname + "=" + cvalue + "; " + expires;
}
執行 setCookie('name', 'jcc', 1)
後,會在只有 www.example.com
作用的 cookie 中加入 name=jcc
字串,並於一天後刪除。
而下次瀏覽器造訪 www.example.com
時,就可以從 cookie 去取得裡面有存的資料,詳細作法請見 W3C 介紹。
上面這個範例是單純應用在瀏覽器端。而 Cookie 的另一個特性是:在向該 domain 的 server 發送請求時,也會被一併帶進去該請求中。因此,記得不要讓 cookie 的內容太多,會增加傳輸量的負擔。另外透過這個特性,可以做到驗證客戶端的功能。
伺服器端的 Cookie
Cookie 的規範中定義了:伺服器端從 request 中接受到 cookie 的訊息,在產生 response 的時候,也會一起回覆給用戶端。
這個行為也告訴我們:在伺服器這邊也可以設定 cookie。由伺服器這邊設定給 cookie 的訊息,可能就會有關身分驗證,因此稍微中斷一下,接下來先來介紹 session。
Session
Session 負責紀錄在 web server 上的使用者訊息。Session 機制會在一個用戶完成身分認證後,存下所需的用戶資料,接著產生一組對應的 id
,存入 cookie 後傳回用戶端。
這個 id 要是獨特的,所以會使用 uuid 的機制,重複的機率非常非常低。
因此當下次用戶端發送請求時,如果帶有該 id
資訊,web server 就會認為該請求是來自該名使用者,達到驗證用戶的目的。這個時候防偽的機制就相當重要,如果伺服器端的實作有問題,再加上用戶端竄改 cookie,就有可能被偽造身分。
假設這個漏洞百出的情況:現在伺服器端單純由 cookie 內設定的 id 名稱來判斷用戶,也就是說,看到 cookie 內 dotcom_user=jcc
,就認為目前用戶是 jcc。如果這樣,只要把 cookie 改成 dotcom_user=messi
,身分馬上就變成 messi 了。如果改成 dotcom_user=admin
呢?說不定就得到了管理者權限。。。
這個時候就要靠簽章來驗證資料的真實性。
Signed Cookie
所以當伺服器端在產生 cookie 時,都會加上 secret 來作 hash,來保證回來的資料沒有被更動過。
假設現在的 cookie 資料是:
{ dotcom_user: 'jcc' }
搭配上一段秘密字串 this_is_my_secret_and_fuck_you_all
,來作 sha1:
var r = sha1('this_is_my_secret_and_fuck_you_all' + 'jcc')
// d01a3d595af33625be4159de07a20b79a1540e54
最後回傳到用戶端的 cookie 為
{
dotcom_user: 'jcc',
'dotcom_user.sig': 'd01a3d595af33625be4159de07a20b79a1540e54'
}
這時如果用戶端更改了 cookie,因為他不知道秘密字串是什麼,所以無法產生正確的 hash 值,因此在校對時就會出錯。這樣就可以避免掉被竄改 cookie 的可能了!
express-session
express app 上可使用 express-session 來輕鬆設定 session 的功能。在產生傳回用戶端包含 session id 的 cookie 時,就有用到上面提到的 signed cookie 概念,因此一定要設定 config 裡的 secret
屬性,另外還有一些額外的設定:
-
path: 那些路徑才會發送該 cookie,預設為
'/'
。 -
httpOnly:設定是不是只允許在 server-side 更改該 cookie(也就是不允許透過 javascript 設定),預設為
true
。 -
secure:支援 https 的情況下開啟可以提高 cookie 安全性,預設為
false
。 -
maxAge:cookie 在多久會過期,是與現在時間的相差,單位為毫秒。如果沒有設定或設為 0,該 cookie 就會是 browser-session cookie,會在關閉瀏覽器後移除。
express-session 有一個 store
,是用來設定 session 的儲存方式,預設為 MemoryStore
。看一下這個警語:
Warning The default server-side session storage, MemoryStore, is purposely not designed for a production environment. It will leak memory under most conditions
表示預設值 MemoryStore
不應用在正式環境,在上線前記得改成其他選項。
Session Store
儲存 session 有下列四種方法,1)內存 2)cookie 3)緩存 4)資料庫。內存就是上面所說的 MemoryStore,有 memory leak 的風險,但適合用在開發環境;cookie 的方式就是把 session 存在 cookie 中,配合簽章防偽,但這個方法會讓 request 傳輸量變大,而且會有回放的風險,不推薦使用;資料庫的方式比較不適合用在 session,有特別的情況再考慮。
因此大部分正式環境,都會用緩存的方式,常見的有 redis 或 memcache。
最後,上面的說明大量了引用這篇文章,寫的相當好,我這篇其實只是換句話說而已,如果對這裡的解說有不了解可以去看看原文。