iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 16
5
Modern Web

Half-Stack Developer 養成計畫系列 第 16

你喜歡吃餅乾嗎?我是還好(session 與 cookie)

你喜歡吃餅乾嗎?我是還好(session 與 cookie)

想到了餅乾,就想到糖果。比起餅乾,其實我更喜歡吃糖果,尤其是小熊軟糖。不過餅乾的確也不錯,我有吃過那種我一輩子都忘不掉的餅乾。

好,至於為什麼會提到餅乾,就是今天會講到一個叫做 cookie 的東西。先讓我來把整個脈絡說清楚吧!

在 HTTP 的世界裡,它是一個「無狀態」的協議。什麼意思呢?那就是每一個 request 都是一個「獨立的」request,彼此之間不會有任何關聯。所以 Server 那邊也不會保存任何狀態。每一個 request 都是一個新的 request。

你可以把伺服器想成是一個喪失記憶能力的人,每一次你去找他的時候,他都當作是第一次見到你,完全忘記你以前有去找他了。所以你會發現,這樣子就會有一個嚴重的問題:登入功能怎麼做?

登入是幾乎每一個網站都有的基本功能,但是如果伺服器記不住你,這個功能就沒辦法完成。為什麼呢?因為登入完成的下一個 request,伺服器就忘記你了。

  1. 我是XXX,我要登入
  2. 伺服器:好,你真的是 XXX,你登入了
  3. 我要拿我的會員資料
  4. 伺服器:先生你哪位?

因此我們需要別的方法來解決這件事情。你能回想出任何一部你看過的類似的電影嗎?男女主角其中一個也是這樣,只有超級短期記憶的人。在他們家裡,都會有很多便利貼你記得嗎?因為便利貼過一天之後不會消失嘛,所以他們就把所有的資訊都寫在上面來提醒自己,早上醒來的時候只要看到便利貼就可以知道發生什麼事了。

伺服器也是這樣,在某一個人登入之後,他要拿一張便利貼寫說:peter 登入了。可是光寫這樣還不夠,伺服器並不知道到底哪一個 request 是 peter,哪一個是 jack,完全分不清楚。所以他還要額外再給一個資訊:一個 key,也就是一把鑰匙。是能證明 peter 這個人的鑰匙。或者你要想成是「令牌」也可以(順帶一提,session 的中文翻譯有人就翻作令牌),在登入完成之後,伺服器會給 peter 一個令牌,下次 peter 來找他的時候帶著這個令牌,伺服器就知道說他真的是 peter 了。其實也有種員工識別證的感覺啦。而在電腦的世界裡,這種令牌其實就是一組隨機的字串,例如說:「grjio390jffwoi32」。

仔細想一想之後你會發現這個流程還不錯,只是需要三個地方的配合:

  1. 伺服器要知道某個令牌是對應到哪個人(寫在便利貼上)。
  2. 瀏覽器要能夠把這個令牌存起來,並且在每一個 request 都帶上。

前者的這個機制就叫做 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 內容的。所以如果我們把這整個登入流程講得更詳細,會是這樣:

  1. 瀏覽器發送帳號密碼給伺服器
  2. 伺服器驗證通過之後,產生一個 session id,並且把這個 id 對應到的內容存在記憶體裡面
  3. 伺服器透過 response header 的 set-ccokie,命令瀏覽器設置 cookie
  4. 瀏覽器收到 set-cookie 這個 header 之後,根據需求設定好 cookie
  5. 等下一個 request,瀏覽器會自動把所有 cookie 一起放在 request 裡面帶上去
  6. 伺服器收到 cookie,檢查有沒有 session id,檢查通過,知道這個人剛才登入過

最詳細的流程差不多就是這樣了。有興趣的可以自己再去找相關文章來研究一下。

最後我們再講一個也是拿來做類似事情的東西,叫做 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)。

來講個最簡化版的好了:

  1. client 發送帳號密碼給 server
  2. server 驗證通過,加密過後生成 rgjio43t43jto439i0ff2f 這一串 JWT 給 client
  3. client 的下一個 request 再帶上 rgjio43t43jto439i0ff2f
  4. server 解密,解出 rgjio43t43jto439i0ff2f 這一串文字是:{username: 'peter'},得知他是 peter

大概就是這樣的一個概念。不過除了這個以外會用 JWT 當然還有其他考量啦,但因為這個主題也可以講個一篇,而且我自己也沒有用過,所以就不野人獻曝了。有興趣的可以參考:什么是 JWT -- JSON WEB TOKEN


上一篇
MV* 的愛恨情仇
下一篇
想~簡簡單單愛:超簡單留言板
系列文
Half-Stack Developer 養成計畫30

1 則留言

0
JeffreyChen
iT邦新手 5 級 ‧ 2017-03-08 09:25:23

勘誤:前提當然是 client 那邊沒有 server 加密用的 keu

Keu>key

huli iT邦新手 5 級 ‧ 2017-03-10 00:26:29 檢舉

感謝指正~

我要留言

立即登入留言