想到了餅乾,就想到糖果。比起餅乾,其實我更喜歡吃糖果,尤其是小熊軟糖。不過餅乾的確也不錯,我有吃過那種我一輩子都忘不掉的餅乾。
好,至於為什麼會提到餅乾,就是今天會講到一個叫做 cookie 的東西。先讓我來把整個脈絡說清楚吧!
在 HTTP 的世界裡,它是一個「無狀態」的協議。什麼意思呢?那就是每一個 request 都是一個「獨立的」request,彼此之間不會有任何關聯。所以 Server 那邊也不會保存任何狀態。每一個 request 都是一個新的 request。
你可以把伺服器想成是一個喪失記憶能力的人,每一次你去找他的時候,他都當作是第一次見到你,完全忘記你以前有去找他了。所以你會發現,這樣子就會有一個嚴重的問題:登入功能怎麼做?
登入是幾乎每一個網站都有的基本功能,但是如果伺服器記不住你,這個功能就沒辦法完成。為什麼呢?因為登入完成的下一個 request,伺服器就忘記你了。
因此我們需要別的方法來解決這件事情。你能回想出任何一部你看過的類似的電影嗎?男女主角其中一個也是這樣,只有超級短期記憶的人。在他們家裡,都會有很多便利貼你記得嗎?因為便利貼過一天之後不會消失嘛,所以他們就把所有的資訊都寫在上面來提醒自己,早上醒來的時候只要看到便利貼就可以知道發生什麼事了。
伺服器也是這樣,在某一個人登入之後,他要拿一張便利貼寫說:peter 登入了。可是光寫這樣還不夠,伺服器並不知道到底哪一個 request 是 peter,哪一個是 jack,完全分不清楚。所以他還要額外再給一個資訊:一個 key,也就是一把鑰匙。是能證明 peter 這個人的鑰匙。或者你要想成是「令牌」也可以(順帶一提,session 的中文翻譯有人就翻作令牌),在登入完成之後,伺服器會給 peter 一個令牌,下次 peter 來找他的時候帶著這個令牌,伺服器就知道說他真的是 peter 了。其實也有種員工識別證的感覺啦。而在電腦的世界裡,這種令牌其實就是一組隨機的字串,例如說:「grjio390jffwoi32」。
仔細想一想之後你會發現這個流程還不錯,只是需要三個地方的配合:
前者的這個機制就叫做 session,令牌通常就叫做 session key 或是 session token,伺服器會把 session 的資訊存在記憶體裡面,當 request 帶 session key 上來的時候,就去記憶體裡面查說這個人是誰,就可以驗明正身了。
後者的話,瀏覽器要把資訊存在哪裡呢?存在一個叫做「cookies」的地方(我也不知道為什麼要叫這個名字,有興趣的朋友可以自己去查查)。你應該對這個詞不陌生才對,有時候網路發生問題,應該都會有人建議你去「清掉 cookie」,意思就是把瀏覽器存的所有資訊都刪除。所以你會發現清掉以後,你所有的網站都被登出了。因為你的所有令牌都被丟掉了,所以伺服器當然不知道你是誰。
原理大概就講到這了,接著我們來實作一下。
首先當然是寫一個可以輸入帳號密碼的表單,然後根據有沒有傳入 username 判斷是否是登入狀態,決定要顯示哪一種版面
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" />
</head>
<body>
<div class="container">
<% if (username) { %>
<h1>Username: <%= username %></h1>
<a class="btn btn-default" href="/logout">登出</a>
<% } else { %>
<form method="post" action="/login">
<div class="form-group">
<label for="username">帳號</label>
<input name="username" class="form-control" id="username" placeholder="username">
</div>
<div class="form-group">
<label for="exampleInputPassword1">密碼</label>
<input name="password" type="password" class="form-control" id="exampleInputPassword1" placeholder="Password">
</div>
<button type="submit" class="btn btn-default">送出</button>
</form>
<% } %>
</div>
</body>
</html>
再來就是寫我們的 express,之前忘記介紹一個東西叫做:「middleware」,中間件。這個就是可以擴充 express 的功能,讓 request 在到達你實作的 function 以前,可以先做一些改變。例如說我們要接收表單 POST 過來的參數,我們就需要body-parser
這個中間件。用了以後,我們就可以在函式裡面透過req.body.username
來拿到表單提交過來的資料。
如果要用到 session 功能的話,可以用express-session
這個中間件,你就有req.session
可以使用了。其他的細節,也就是我們今天講的原理的那些東西他都幫你實作好了,你只要照著用就行了。
先來安裝一下必要的東西:
npm install express express-session body-parser --save
再來直接開始寫 code:
var express = require('express');
var session = require('express-session')
var bodyParser = require('body-parser')
var app = express();
// 使用 session,要設定一個 secret key
app.use(session({
secret: 'keyboard cat',
}))
// 有了這個才能透過 req.body 取東西
app.use(bodyParser.urlencoded({ extended: false }))
app.set('view engine', 'ejs');
// 首頁,直接輸出 index
app.get('/', function(req, res) {
// 試著看看 session 裡面有沒有 username 可以拿
var username = req.session.username;
res.render('index', {
username: username
});
});
// 登入,如果帳號密碼是 peter 123 就登入通過
app.post('/login', function(req, res) {
var username = req.body.username;
var password = req.body.password;
if (username === 'peter' && password === '123') {
console.log('login success');
req.session.username = 'peter';
}
res.redirect('/');
})
// 登出,清除 session
app.get('/logout', function(req, res) {
req.session.destroy();
res.redirect('/')
})
app.listen(3000, function () {
console.log('Example app listening on port 3000!')
})
都完成之後,來觀察一下背後到底是怎麼運作的,有一個指令叫做curl
,沒有的話可以自己去找怎麼安裝,是個很好用的工具。
可以用 curl -I -X POST http://localhost:3000/login
來看看會有什麼 response header:
HTTP/1.1 302 Found
X-Powered-By: Express
Location: /
Vary: Accept
Content-Type: text/plain; charset=utf-8
Content-Length: 23
set-cookie: connect.sid=s%3A0OGS7o0LhXlSBCKXhd4bsWHHfdBrskFw.l3xDwlhdkWyyBcmXJVlAP4Die52p0UeGYhN4TFzISVA; Path=/; HttpOnly
Date: Wed, 21 Dec 2016 14:15:07 GMT
Connection: keep-alive
其他你都可以忽略,重點是set-cookie: connect.sid=...
這一行。瀏覽器就是靠這個set-cookie
來設定 cookie 內容的。所以如果我們把這整個登入流程講得更詳細,會是這樣:
set-ccokie
,命令瀏覽器設置 cookieset-cookie
這個 header 之後,根據需求設定好 cookie最詳細的流程差不多就是這樣了。有興趣的可以自己再去找相關文章來研究一下。
最後我們再講一個也是拿來做類似事情的東西,叫做 JWT, JSON Web Token。JSON 是一種資料格式的名稱,你就想成是 JavaScript 的物件就好,例如說:
{
status: 'OK',
reply: {
users: [
{
id: 1,
name: 'heelo'
}, {
id: 2,
name: 'world'
}
]
}
}
現在很多 client 跟 server 間都是透過 JSON 這個簡潔的格式交換資料。幾乎所有程式語言都有 library 可以來處理 JSON 類型。
那 JSON Web Token 就是什麼呢?你可以想成是跟剛剛我們所介紹的 session 是很類似的一個技術,差別在於原本伺服器那邊儲存的資訊,現在改由 client 端來儲存。例如說上面例子的username
,現在就不存在 server 的 session 裡,而是直接存在 JWT 裡面。而 JWT 有規範了加密方式跟格式,所以可以保證 client 端無法竄改(前提當然是 client 那邊沒有 server 加密用的 key)。
來講個最簡化版的好了:
大概就是這樣的一個概念。不過除了這個以外會用 JWT 當然還有其他考量啦,但因為這個主題也可以講個一篇,而且我自己也沒有用過,所以就不野人獻曝了。有興趣的可以參考:什么是 JWT -- JSON WEB TOKEN
勘誤:前提當然是 client 那邊沒有 server 加密用的 keu
Keu>key
感謝指正~
這倒是確實,跟我現在的風格好像差滿多的,有點懷念XD