Mike Huang总结
本文介绍了如何通过 JSON Web Token(JWT)实现身份验证机制,并详细说明了 JWT 的结构、生成和验证方法,以及如何在 Node.js 项目中实现用户注册、登录、登出等功能。
摘要
文章首先介绍了传统的基于 Session 和 Cookie 的身份验证机制,并指出了其在用户量增加时对数据库性能和成本的影响。随后,提出了 JWT 作为一种更符合 RESTful API 设计原则的无状态身份验证解决方案。JWT 由三部分组成:Header、Payload 和 Signature,分别用于声明 Token 的类型和算法、存储用户信息以及验证信息的完整性和真实性。
文章接着介绍了如何使用 jsonwebtoken 库在 Node.js 中生成和验证 JWT。通过 sign() 方法可以生成一个包含用户信息和过期时间的 Token,并通过自定义密钥进行签名。在客户端,用户在成功认证后需要将接收到的 JWT 存储起来,并在后续的请求中通过 HTTP 头部的 Authorization 字段携带 Token。服务器端则通过 verify() 方法对 Token 进行验证。
文章最后详细描述了如何在一个 Task App 项目中实现注册、登录、登出以及登出所有设备的路由,并使用 Mongoose 与 MongoDB 数据库进行交互。同时,介绍了如何创建一个路由保护中间件,以确保只有经过身份验证的用户才能访问特定的 API 端点。此外,文章还提供了一些其他资源,供读者进一步学习 JWT。
观点
- JWT 优于传统的 Session 和 Cookie 身份验证机制,因为它不需要在服务器端保存用户的认证状态,减少了对数据库的依赖,提高了应用的可扩展性和性能。
- JWT 的无状态特性使得它能够更好地与 RESTful API 原则相结合,每个请求都包含了所有必要的认证信息。
- JWT 的安全性:虽然 Payload 部分是可以被解码的,但由于 Signature 部分是使用服务器端的密钥生成的,因此任何对 Payload 的篡改都能在服务器端验证时被检测到。
- 在实际项目中实现 JWT 身份验证,需要在用户模型中添加生成和验证 Token 的方法,并且在用户注册和登录时生成 Token,在登出时从数据库中移除 Token。
- 路由保护中间件的重要性,它确保了只有带有有效 JWT 的请求才能访问受保护的路由,从而保障了 API 的安全性。
- JWT 的使用可以提高用户体验,因为它减少了服务器的响应时间,使得 API 更加高效。
- 文章提供的其他资源,可以帮助读者更深入地了解 JWT 的工作原理和最佳实践。
[筆記] 透過 JWT 實作驗證機制

過去在專案中大多透過 Session 和 Cookie 實作驗證機制(例如 👉 透過 Passport.js 實作驗證機制),在使用者成功登入後,會在伺服器端建立 Session,並提供客戶端相對應的 Session ID。往後來自客戶端的請求,伺服器以 Cookie 中 Session ID 尋找 Session 確認使用者驗證狀態。然而,使用這個方法,意味著每位用戶經過驗證後,都需要為其建立和存取 Session(可能在資料庫中)當用戶量增加,資料庫的花費成本將不斷提高;每當用戶拜訪需經驗證的路徑時,也需要額外到資料庫執行資料查詢,花費額外的效能。
透過 Session 和 Cookie 實作驗證機制JSON Web Token(JWT)也因此誕生,它更符合設計 RESTful API 時「Stateless 無狀態」原則:意味著每一次從客戶端向伺服器端發出的請求都是獨立的,使用者經驗證後,在伺服器端不會將用戶驗證狀態透過 Session 儲存起來,因此每次客戶端發出的請求都將帶有伺服器端需要的所有資訊 — 從客戶端發出給伺服器端的請求將帶有 JWT 字串表明身份 。整個驗證的流程大致會是:
- 伺服器端在收到登入請求後驗證使用者
- 伺服器端產生和回傳一組帶有資訊,且僅能在伺服器端被驗證的 Token
- Token 被回傳後,存取在「客戶端」(大多存在瀏覽器的 Storage 當中)
- 往後客戶端向伺服器端發送請求時,皆附帶此 Token 讓伺服器端驗證
- 若伺服器端在請求中沒有找到 Token,回傳錯誤;若有找到 Token 則驗證
驗證流程圖示JWT 組成
JWT 是一組字串,透過(.)切分成三個為 Base64 編碼的部分:
- Header:含 Token 的種類及產生簽章(signature)要使用的雜湊演算法
- Payload:帶有欲存放的資訊(例如用戶資訊)
- Signature:編譯後的 Header、Payload 與密鑰透過雜湊演算法所產生
// base64(Header) + base64(Payload) + base64(Signature)
// xxxxx.yyyyy.zzzzz
Header
Header 是一個包含定義 Token 種類(type)及雜湊演算法(alg)資訊的 JSON。在此設定 Token 種類為 JWT、產生簽章(signature)要使用的雜湊演算法為 HS256。此 JSON 將被轉換成 Base64 編碼,成為第一個部分:
{
"alg": "HS256",
"typ": "JWT"
}Payload
Payload 也是一個 JSON,使用者和相關的資訊都可以放置其中。通常會使用 exp 設定 Token 到期的時間、iat 設定 Token 簽發時間。最後再被轉換成 Base64 編碼,成為第二個部分:
{
"_id": "<user_id>",
"name": "Mike",
"exp": 1300819380
}Payload 又可以被稱為 claims,能想成使用者是透過附帶 JWT 通過認證,因此能說這筆資訊是屬於他(她)的。
Payload 和 Header 被轉換成 Base64 編碼後,能夠被輕易的轉換回來
因此不應該把如用戶密碼等重要資料存取在 Payload 當中
Signature
簽章(Signature )是將被轉換成 Base64 編碼的 Header、Payload 與自己定義的密鑰,透過在 Header 設定的雜湊演算法方式所產生的。
由於密鑰並非公開,因此伺服器端在拿到 Token 後,能透過解碼,確認資料內容正確,且未被變更,以驗證對方身份。
安裝 jsonwebtoken
這次使用的是 jsonwebtoken 套件,透過這個套件能更方便的創建、完成驗證 JWT,首先透過 npm 安裝套件:
$ npm install jsonwebtoken
載入 jsonwebtoken
const jwt = require('jsonwebtoken')產生 JWT
透過模組上的sign()方法可以產生一組 JWT,產生時需要將存放在 Token 當中的資料帶入payload參數中、密鑰帶入secretOrPrivateKey參數中:
jwt.sign(payload, secretOrPrivateKey, [options, callback])
👉 options參數非必填,但透過帶入options物件能設定許多選項。例如:
algorithm:設定產生簽章要使用的雜湊演算法(預設: HS256)expiresIn:設定 Token 多久後會過期(自動在 Payload 新增 exp)noTimestamp:設定產生 JWD 時不會自動在 Payload 中新增iat時間
👉 callback 參數非必填,但若要以非同步方式產生 JWD,可以提供一個 Callback 函式,Token 將能在 Callback 函式中取得。
👉 以下範例中,僅將使用者 ID存入 Token、設定 Token 時效為一天,並帶入自訂密鑰(SECRET),產生一組 Token
查看產生的 JWT
客戶在通過認證後,會收到來自伺服器所回傳的 JWT,並將其存取起來(可能是存在 localStorage 當中)。往後在發送請求時,需要在 Request Header 中設定Authorization,將 JWT 一併帶入送至後端伺服器。Authorization的格式通常由 Token 類型(Type)+ 空格 +JWT 所組成:
Authorization: <type> <credentials>
客戶端請求中帶入 JWT
JWT 是一種 Bearer Token,因此在Authorization帶入:
Authorization: 'Bearer ' + token

伺服器端取得 JWT
從來自客戶端請求的 Header 可以獲取Authorization所附帶的完整值,需透過replace()方法處理,最終獲取 JWT 輸出
輸出 JWT
以上方法能產生一組如下的 JWT,透過(.) Token 能分為三個部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1ZDRlOGQ5YTA2MWMxYTJjMDIxY2JlMTgiLCJpYXQiOjE1NjU4NTczMjAsImV4cCI6MTU2NTk0MzcyMH0.GQVyQJLmwXd2jQZsjZ8n6cAWD0HQGjvlp2Mk8kAsGy8
確認 JWT
Token 的三個部分都是 Base64 編碼,因此可以到 jwt.io 將其轉換回原來的 JSON 資料。簽章部分由於還透過與私鑰,經雜湊演算法產生,因此可以將自訂私鑰帶入「VERIFY SIGNATURE」區塊,驗證該 Token 的正確性:

❗️使用 JWT 的重點不在於把資料隱藏起來,畢竟 Payload 資料是可以被轉換回來的
但由於伺服器端才擁有密鑰,因此即使 payload 被修改,轉換成 Base64 重新置入 Token 中,透過與 Signature 比對之下,就能發現資料的不一致,產生驗證錯誤
驗證 JWT
透過模組上的verify()方法可以完成 Base64 解碼與 Token 的驗證,並回傳解碼後的 Payload — 驗證時需要帶入欲驗證的token與自訂的密鑰:
jwt.verify(token, secretOrPublicKey, [options, callback])
👉 以下範例中,將來自客戶端的 Token 和自訂密鑰(SECRET)帶入 verify()函式中,並將回傳的 Payload 存放在decoded中:
專案實作
最近在學習和實作 RESTful API 打造 Task App,其中驗證機制就是透過 JWT 實現,因此以下將紀錄使用 jsonwebtoken 套件,實作註冊、登入、登出、登出所有裝置四條路由,及建立 Route Protection Middleware 完成驗證機制。
初始化專案
在專案初始時,透過 Mongoose 連線 MongoDB 資料庫、將已經建立好的使用者 Model 引入、建立了註冊、登入、登出、登出所有裝置四條路由、啟動並監聽伺服器:
👉 專案會額外使用到的套件:bcrypt、jsonwebtoken
註冊路由
使用者發出註冊請求到伺服器端後,從請求中req.body取出驗證資訊,並儲存至資料庫當中。成功建立與儲存資料後,回傳 Status Code 201 與建立之用戶資料。
密碼處理
將使用者資料存至資料庫以前,先將密碼透過 bcrypt 處理,獲取一組 hashed password 再存至資料庫較安全。在很多專案中,除了註冊時會需要處理密碼,也可能會有修改密碼的功能。因此直接使用 Mongoose 提供的 Middleware 統一包裝密碼處理功能:
- 在
userSchema上使用 Pre middleware:監聽指定的事件(第一個參數),並在展開該事件(pre)以前,執行定義的函式(第二個參數) - Pre middleware 帶入
save:監聽文件被存取的事件 - 在 Middleware 當中
this指向目前正被儲存的使用者文件 - 透過文件上
isModified() 方法:確認文件在被儲存時,password欄位是有被變更的(變更包含初次建立密碼和修改密碼時) - 若確認密碼欄位有被變更,透過 bcrypt 處理密碼
產生 JWT
註冊和登入成功後,都算完成驗證,因此伺服器端將產生一組 JWT 回傳給客戶端。由於會需要在超過一個以上的路由中「為當前使用者產生 JWT」,因此直接在「使用者實例(instance)」上建立可共用的方法(methods)
- 建立方法:在
userSchema的methods物件上建立generateAuthToken函式 - 建立的方法中
this指向呼叫此方法的使用者實例(instance) - 將產生的 JWT 存入資料庫,讓使用者能跨裝置登入,也能登出單一裝置
- 此方法將回傳當次產生的 JWT
新增欄位存放 JWT
在userSchema中新增存放 JWT 的tokens欄位,此欄位的資料格式是一個陣列,其中帶有一個個含有token的物件:
加入註冊路由中
將註冊用戶的資訊建立並儲存至資料庫後,透過使用者實例(instance)的generateAuthToken方法,為此用戶產生一組 JWT 存至token上。伺服器回傳時,不僅回傳成功註冊的用戶資訊,也將 JWT 回傳給客戶端。
測試路由
在 Postman 中透過POST方法向/users路徑發出請求,並在 Body 中帶入註冊所需資訊。回傳後,確認 Body 中包含用戶資訊及 JWT 資料。

在 Robo 3T 中也能確認該用戶的資料及 Token 有被成功存至資料庫中:

登入路由
使用者發出登入請求到伺服器端後,會從請求中req.body取出驗證資訊,通過驗證(待完成)後,同樣為使用者產生 JWT 一併回傳給客戶端。
建立驗證方法
在此欲將驗證功能,獨立寫成一個可以重複被使用的函式,因此直接在使用者 Model 上建立一個驗證方法(statics)。
- 建立方法:在
userSchema的statics物件上建立findByCredentials函式 findByCredentials函式接收兩個參數:使用者email和password- 若資料庫中無該名使用者或密碼驗證失敗,皆會丟出錯誤訊息
- 成功驗證後,此方法將回傳當次使用者完整資料
📍 methods(instance methods) vs statics(model methods)
Document(物件實例)的共用方法將被建立在 userSchema 的 methods 物件上
Model 使用的方法將被建立在 userSchema 的 statics 物件上
加入登入路由中
將欲驗證的使用者 email 和密碼,帶入使用者 Model 的findByCredentials方法中,並把驗證成功回傳的使用者資訊存至user上。最終伺服器回傳用戶資訊和 JWT 給客戶端。
測試路由
在 Postman 中透過POST方法向/users/login路徑發出請求,並在 Body 中附帶登入所需資訊。回傳後,確認 Body 中包含用戶資訊及 JWT 資料。

Route Protection
在設計路由時,有些路徑將僅提供經過驗證的使用者才能拜訪,因此將建立一個驗證用的 Middleware,並將其置入所有需擁有權限才可拜訪的路由中 — Route Protection:
- 從
req.header中取得和擷取 JWT 驗證 - Token 驗證成功後,到資料庫找尋符合用戶 id 和 Tokens 欄位中包含此 Token 的使用者資料
- Token 驗證成功且找到符合的使用者資料後,分別存至
req.token和 req.user上供後續使用
將驗證 Middleware 加入路由中
建立了一個驗證用的 Middleware 後,就能將其放置於所有需權限才能拜訪的路由中。例如:若使用者想登出,前提是該名使用者是經過驗證狀態,才能執行登出的行為:
- 將驗證 Middleware 載入
- 在登出路由中加入驗證 Middleware
登出路由
登出的運作方式就是將當次的 Token 從使用者資料中 tokens 欄位刪除— 使用者發出登出請求到伺服器端後,會先通過auth驗證 Middleware,因此能在請求中取得user和token資料來使用:將當前的 Token 從使用者 Tokens 欄位資料中篩掉,並存回資料庫當中。
測試路由
在 Postman 中透過POST方法向/users/logout路徑發出請求,並在 Request Header 中設定Authorization,附帶 JWT 資訊。回傳後,確認 Status Code 為 200 OK:

登出所有裝置路由
登出的運作方式就是將所有 JWT 從使用者資料中 tokens 欄位刪除
結語
透過這次的實作,很開心能透過不同的方式實作驗證機制。運用 JWT 來實現與運用 Session + Cookie 的運作方式有很多不同之處。以 JWT 可以解決資料庫擴展性問題,並提升效能以增加用戶體驗,且更能符合設計 RESTful API 時「Stateless 無狀態」原則。
其他資源
JSON Web Token (JWT) — The right way of implementing, with Node.js
So what the heck is JWT or JSON Web Token?
PJCHENder 私房菜 — [Note] JWT 學習筆記
JSON Web Token — 在Web应用间安全地传递信息
RESTful Web服務無狀態
使用jwt实现restful接口用户验证
Nodejs
Jwt
Json Web Token
Authentication
Notes
Recommended from ReadMedium