NUK JavaScript Lesson 12:在 Express 上應用 Session 實作登入功能

本篇筆記會介紹如何在 Express 上使用 express-session 模組來應用 Session,並實作出會員登入功能。

什麼是 Session

以 Facebook 為例,一般來說我們可以在 Console → Application → Cookies 中取得 c_user,也就是使用者編號 (ID),這時只要再取得登入狀態 (spin) 與 xs 就能成功登入帳號。
而在我們登入後,因為有 Session 的關係,所以這個帳號只有在網頁或伺服器重新啟動時,才會重置資料,像這種應用就是 Session 的普遍的使用方式。

express-session 簡單介紹

想要在 Express 中實作出 Session 的效果,可以使用第三方模組 express-session。

  • 安裝:npm install express-session --save

  • 初始化設定:在 app.js 撰寫以下的程式碼

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var session = require('express-session'); // 讓 Express 有 Session 的功能
    app.use(
    session({
    secret: '1q3rrwefsgdh54uu5h56', // 加密
    resave: false,
    saveUninitialized: true,
    cookie: { maxAge: 1000 * 15 }, // 時間可更改
    })
    );

接下來就透過一些實作練習,來開始使用 express-session 吧!

實作應用:登入功能

假登入

實作功能:登入後記錄 Cookie,過一段時間後清除。

  1. 在 index.js 增加兩個路由,來測試 Session

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // (2) 取得 Server 端的 Session
    router.get('/', function (req, res, next) {
    res.render('index', {
    title: 'express',
    name: req.session.name,
    password: req.session.password,
    });
    // (1) 自訂 Session 到 Server 端儲存
    router.post('/', function (req, res) {
    // 自訂瀏覽器"暫時"要寫什麼資訊進去
    req.session.name = req.body.name;
    req.session.password = req.body.password;
    res.redirect('/')
    })
  2. 製作 index.ejs 呈現 Session 的值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <!-- ...omit -->
    <body>
    <p><%= name %></p>
    <p><%= password %></p>
    <p><%= memberId %></p>
    <h2>假登入</h2>
    <!-- 傳送兩個值 name, email -->
    <form action="/" method="post">
    <input type="text" name="name" value="">
    <input type="text" name="password" value="">
    <input type="submit" name="" value="送出">
    </form>
    </body>
    <!-- omit... -->

完成後開啟 http://localhost:3000/ 進行假登入,輸入的帳號密碼會作為 Session 顯示在標題上方。

不過這只是假的登入功能,因為我們沒有連接到資料庫做驗證。
真正的登入應該還要經過 Server 端的資料庫做驗證、比對帳密後,才能完成登入的動作。

真登入

我們可以透過 Firestore 建立假資料庫,使用我們自訂的帳密做驗證比對,實作出真正的登入功能。

Firestore + express-session

  1. 為 index.ejs 新增真登入的表單欄位

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!-- ...omit -->
    <body>
    <p><%= name %></p>
    <p><%= password %></p>
    <p><%= memberId %></p>
    <h2>假登入</h2>
    <form action="/" method="post">
    <input type="text" name="name" value="">
    <input type="text" name="password" value="">
    <input type="submit" name="" value="送出">
    </form>
    <h2>真登入</h2>
    <form action="/login" method="post">
    <input type="text" name="name" value="">
    <input type="text" name="password" value="">
    <input type="submit" name="" value="送出">
    </form>
    </body>
    <!-- omit... -->
  2. 在 index.js 中使用 Firestore 加上 express-session 製作登入的路由

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    router.post('/login', function (req, res) {
    var memberRef = db.collection('member');
    memberRef
    .where('account', '==', req.body.name)
    .get()
    .then(function (querySnapshot) {
    querySnapshot.forEach(function (doc) {
    // 以下三行是測試用的
    console.log(doc.data().password == req.body.password);
    console.log(doc.data().password);
    console.log(req.body.password);
    if (doc.data().password == req.body.password) {
    req.session.memberId = doc.id;
    res.redirect('/');
    }
    });
    });
    });
    • 資料驗證方式:
      當資料庫裡的 account 與該筆表單送出的 name 相同時,再去驗證資料庫裡的 password 是否也與表單的 password 相同。
      如果都符合,就把資料庫的該筆文件的 id 當作 memberId 一併呈現於畫面上。

練習題:取得會員的購物清單

實作功能:會員登入後,列出該位會員的產品購買清單。

重要觀念整理

  • doc.data() 會回傳該筆文件的整個內容,可再存取 doc.data().namedoc.data().password 等個別欄位的資料內容。
  • doc.id 會指向該筆文件的 ID,像是 4NK3b5G0wf5jOLo62pN8 這種亂碼。
  • let docRef = db.collection("member").doc("singleObjectWithDataFields").doc() 是指向該筆文件的 “參考”。

在開始之前,要先建立好資料集,以下為會員購物清單 orderlist 資料集的截圖。

Firestore

最左邊是每個會員的亂碼 ID,每個會員除了有基本資料的欄位外,都還有一個集合 orderlist,這是代表他們每個人的購物清單。

如果想要存取到 orderlist 裡面的文件內容,可以透過這段程式碼來取得:let orderlistRef = db.collection("member").doc(doc.id).collection("orderlist")

實作步驟說明

  1. 透過 db.collection("member") 存取 member 資料集內的文件
  2. doc.id 等於每一位會員的亂碼 ID
  3. 接著的 .doc(doc.id) 即可取得該 ID 的參考位置
  4. 再透過 .collection("orderlist") 取得參考位置裡的資料集 orderlist
  5. 用空陣列 orderData 接取文件裡的購買資料

最後我們就把需要的資料 render 出來,讓 member.ejs 替我們渲染出結果。
透過兩次取得資料集的方式,我們就能成功拿到包在資料集裡面的資料集的資料囉!

範例程式碼

member.ejs

1
2
3
4
5
6
7
8
9
10
<!-- ...omit -->
<body>
<h2><%= userName %> 的購買清單</h2>
<ul>
<% for(let i =0; orderData.length>i; i++){ %>
<li><%- orderData[i].name %><%- orderData[i].price %></li>
<% } %>
</ul>
</body>
<!-- omit... -->

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
router.post('/login', function (req, res) {
var memberRef = db.collection('member');
let order_data = [];
// 驗證 member 資料集的帳號密碼
memberRef
.where('account', '==', req.body.name)
.get()
.then(function (querySnapshot) {
querySnapshot.forEach(function (doc) {
if (doc.data().password == req.body.password) {
// 每個會員的 orderlist 資料集
let user_name = doc.data().name;
let orderlistRef = db
.collection('member')
.doc(doc.id)
.collection('orderlist');
orderlistRef.get().then(function (querySnapshot) {
querySnapshot.forEach(function (doc) {
order_data.push(doc.data());
});
res.render('member', {
userName: user_name,
orderData: order_data,
});
});
}
});
});
});

以上資源是我自己整理過後的筆記,若有錯誤歡迎隨時和我聯繫。