# Express.js 身分驗證篇 - Session & JWT Token
身分驗證篇,文章中包括 Session 及 JWT Token 的基本使用方法。
## 什麼是身分驗證
身分驗證又稱身分認證、鑒權,是指通過一定的手段完成對用戶身分的確認。
- 日常生活中的身分認證有:手機密碼、指紋解鎖...etc。
- 在 Web 開發中涉及到用戶身分驗證的情況有:各大網站的手機驗證碼登入、郵箱驗證碼登入、QR Code 登入...etc。
### 為什麼需要身分驗證
為了確認當前聲稱為某種身分的用戶,確實是所聲稱的用戶。例如領包裹的時候,必須出示身分證證明式本人的包裹才可以領取。
### 不同開發模式下的身分驗證
對於伺服器渲染和前後端分離這兩種開發模式來說,分別有著不同的身分驗證方案:
1. 伺服器渲染推薦使用 Session 驗證機制
2. 前後端分離推薦使用 JWT 驗證機制
## Session 驗證機制
### HTTP協議的無狀態性
了解 HTTP 協議的無狀態性是進一步學習 Session 驗證機制的必要前提。
HTTP協議的無狀態性,指的是 client 端的每次 HTTP 請求都是獨立的,連續多個請求之間沒有直接的關係,Server 不會主動保留每次 HTTP 請求的狀態。
### 如何突破HTTP無狀態的限制
以超市為例,為了方便收銀員在結帳時給 VIP 打折,超市可以為每個 VIP 發放會員卡。現實生活中的會員卡身分驗證方式,在 Web 開發中的專業術語叫做 Cookie。

### 什麼是 Cookie
Cookie 是存儲在用戶瀏覽器中的一段不超過 4KB 的字串,它由一組鍵值對和其他幾個用於控制 Cookie 有效期、安全性、使用範圍的可選屬性組成。
- 每個不同的域名裡面的 Cookies 都是各自獨立的,**不同域名的 Cookies 不能互相訪問。**
- 每當 Client 端發起請求時,會**自動**把當前域名下所有未過期的 Cookie 一同發送到 Server。
- **開發時不需要做任何事**,比如 JWT 還需要回傳 token,但 Session 不用做任何事

**Cookie 的特性**:
1. 自動發送
2. 域名獨立
3. 過期時限
4. 4KB限制
### Cookie 在身分驗證中的作用
Client 端第一次請求 Server 時,Server 通過響應頭的形式向 Client 端發送一個身分驗證的 Cookie,Client 端會自動將 Cookie 保存在瀏覽器中。
隨後,當 Client 端瀏覽器每次請求 Server 時,瀏覽器會自動將身分驗證相關的 Cookie 通過請求頭的形式發送給 Server,Server 就能驗明 Client 端的身份。

### Cookie不具有安全性
由於Cookie是存儲在瀏覽器中的,而且瀏覽器也提供了讀寫 Cookie 的 API,因此 Cookie 很容易偽造,不具有安全性。因此不建議 Server 將重要的隱私數據通過 Cookie 的形式發送給瀏覽器。
### 提高身份驗證的安全性
以剛剛超市收銀的例子繼續說明,為了防止客戶偽造會員卡,收銀員在拿到客戶出示的會員卡之後,可以在收銀機上進行刷卡驗證,只有收銀機確認存在的會員卡才能正常使用。
這種 會員卡 + 刷卡驗證 的設計理念就是 Session 驗證機制的精隨。
### Session 的工作原理

## 在 Express 中使用 Session 驗證
### 配置 express-session 中間件
安裝 express-session:
```
npm i express-sseesion
```
使用`app.use()` 來註冊 session 中間件:
```javascript=
const session = require("express-session");
app.use(session({
secret: '加密內容',
resave: false, // 固定寫法
saveUninitialized: true // 固定寫法
}));
```
### 向 session 中存資料
當 express-sessionn 中間件配置成功後,即可通過 `req.session` 來訪問和使用 session 物件,從而獲取使用者的關鍵資訊。
- 下方簡單舉一個例子,我們在登入時將 body 的內容存進 session 物件的 user 屬性中,然後將 isLogin 屬性設為 true,這樣當我們在呼叫需要身分驗證的 API 時,就透過判斷 `req.session.isLogin` 是不是 true 再繼續進行操作。
- 若需要獲取使用者資訊,再從 `req.session.user` 中獲取即可。
```javascript=
// 登入時將 body 內容存進 session, 以及將 isLogin 改為 true
app.post("/login", (req, res) => {
req.session.user = req.body; // 將使用者資料存到 session 中
req.session.isLogin = true; // 將使用者的登入狀態存到 session 中
res.send({ status: 0, msg: '登入成功' });
})
// 透過判斷 req.session.isLogin 是否為 true 來確認是否有權限呼叫此 API
app.get("/user", (req, res) => {
if (!req.session.isLogin) {
return res.status(403).send({ status: 1, msg: "無權進行此操作" })
}
res.send({ status: 0, msg: "獲取成功", username: req.session.user.username });
});
// 登出後, 清空 session 訊息
app.post("/logout", (req, res) => {
req.session.destroy(); // 清空 session 訊息
res.send({
status: 0,
msg: "登出成功"
});
});
```
用 postman 嘗試 `post /login` 之後會在下方 cookie 中發現多出了一個 name 為 `connect.sid` 的 cookie:

接著再 `get /user`,如果 session 驗證通過,則會獲得「獲取成功」的響應,否則為「**無權進行此操作**」。
再嘗試登出 `post /logout`,POSTMAN 中的 cookie 仍舊存在,但是該 cookie 實際上已被銷毀,再次呼叫 `get /user` 會顯示「**無權進行此操作**」。
## JWT 認證機制
### 了解 Session 認證的侷限性
Seesion 認證機制需要配合 cookie 才能實現,由於 Cookie 默認不支持跨域訪問,所以當涉及到前端跨域請求後端 API 時,需要做很多額外的配置才能實現跨域 Session 認證。
**注意**:
- 當前端請求後端 API 存在跨域問題時,推薦使用 JWT 驗證機制。
- 當前端請求後端 API 不存在跨域問題時,推薦使用 Session 驗證機制。
### 什麼是 JWT
JWT 全稱 JSON Web Token,是目前最流行的跨域驗證解決方案。
### JWT 的工作原理

**總結**:
1. 使用者的資料通過 Token 字串的形式保存在瀏覽器中,後端通過還原 Token 字串的形式來驗證使用者的身分。
2. JWT 將使用者資料保存在瀏覽器端,Session 則是把使用者資料保存在伺服器端。
### JWT 的組成部分
JWT 通常由三部分組成,分別是 Header, payload, Signature,三者之間用英文的 `.` 分隔,格式如下:
```
Header.Payload.Signature
```
可以在 [JWT.io](https://jwt.io/) 嘗試轉換一下 Token:

紅色部分為 Header, 紫色部分為 Payload, 藍色部分為 Signature。
**總結**:
- Payload 部分才是真正的使用者訊息,它是用使用者資料經過加密之後產生的字串。
- Header 和 Signature 是安全性相關的部分,只是為了保證 Token 的安全性。
### JWT 的使用方式
Client 端收到從後端傳回來的 Token 後通常會將它存在 localStorage 或者 SessionStorage,此後,Client 端和 Server 端通信都要帶上這個 Token 從而進行身分驗證。
推薦的做法是把 Token 存在 HTTP 請求頭的 Authorization 字段中。
```
Authorization: Bearer <Token>
```
## 在 Express 中使用 JWT
### 安裝 JWT 相關的 package
- [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken) 用於生成 JWT 字串
- [express-jwt](https://www.npmjs.com/package/express-jwt) 用於將 JWT 字段解析並還原成 JSON 物件
```
npm i jsonwebtoken express-jwt
```
### 導入 JWT 相關 package
```javascript=
const jwt = require("jsonwebtoken");
const expressJWT = require("express-jwt");
```
### 定義 secret 秘鑰
為了保證 JWT 字串的安全性,防止 JWT 字串在傳輸過程中被別人破解,我們需要專門定義一個用於加密和解密的 secret 秘鑰:
1. 當生成 JWT 字串的時候,需要使用秘鑰對使用者資料進行加密,最終得到加密後的 JWT 字串。
2. 當把 JWT 字串解析成 JSON 物件的時候,需要使用秘鑰進行解密。
```javascript=
const secretKey = 'DEMO';
```
### 在登入成功後生成 JWT 字串
調用 **jsonwebtoken** 提供的 `sign()` 方法,將使用者資料加密成 JWT 字串:
```javascript=
app.post("/login", (req, res) => {
const payload = {
username: req.body.username
}
const token = jwt.sign(payload, secretKey, { expiresIn: '60s' });
res.send({
status: 200,
message: "登入成功!",
token
});
});
```
### 將 JWT 字串還原為 JSON 物件
Client 端每次在訪問那些有權限 API 的時候,都需要主動通過請求頭中的 Authorization 字段將 token 字串送到 Server 端進行身分驗證。
此時 Server 端可以通過 express-jwt 這個中間件自動將 Client 端發送過來的 Token 解析還原成 JSON 物件。
```javascript=
app.use(expressJWT({ secret: secretKey, algorithms: ['HS256'] }).unless({ path: [/^\/api\//] }));
```
1. `expressJWT({ secret: secretKey, algorithms: ['HS256'] })` 就是用來解析 Token 的中間件。
2. `unless({ path: [/^\/api\//] })` 用來指定哪些 API 不需要訪問權限。
### 使用 req.user 獲取使用者資料
當 express-jwt 這個中間件配置成功之後,即可在那些有權限的 API 中,使用 `req.user` 物件來訪問從 JWT 字串中解析出來的使用者資料:
```javascript=
app.get("/admin/getInfo", (req, res) => {
console.log(req.user);
res.send({
status: 200,
message: "獲取使用者資料成功",
data: req.user
});
});
```
> **注意**:千萬不要把密碼加密到 Token 字串中。
現在嘗試 `POST /api/login` 登入看看,會獲得 Token 字串:
```
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNjQ4OTg1MzY5LCJleHAiOjE2NDg5ODUzOTl9.alOYp9U0amH1cwIKzIIjC0JdS6cgDto-bp7_V5SLGrY"
```

再呼叫 `get /admin/getInfo` 獲取使用者資料:

如果沒有 token 會報錯:
```
UnauthorizedError: No authorization token was found
```
而如果 Token 過期後再次請求也會報錯:
```
UnauthorizedError: jwt expired
```
### 捕獲解析 JWT 失敗後產生的錯誤
當使用 Express-jwt 解析 token 字串時,如果 client 端發送過來的 Token 字串過期或不合法,會產生一個解析失敗的錯誤,影響整個項目的運行。
我們可以通過 Express 的錯誤中間件捕獲這個錯誤並進行相關的處理:
```javascript=
//....
app.use((err, req, res, next) => {
if (err.name === "UnauthorizedError") {
return res.send({ status: 401, message: "Token 無效" });
}
res.send({ status: 500, message: "未知錯誤" });
});
```
