[筆記] 透過 Passport.js 實作驗證機制
Passport 介紹
Passport 用途
透過 Node.js 和 Express 在打造應用程式時,驗證系統是很常見的需求,這時 Passport 就是一套能發揮上的好工具。簡單來說,能把 Passport 想成是一個「處理驗證的 middleware」,因此能很容易的整合到 Express 當中 。
Strategy 的搭配
打造驗證系統時,一定會想到的問題是:「我應該要透過帳號和密碼的方式來打造,還是透過常見第三方驗證 — Facebook、Google、Twitter — 來打造驗證機制?」Passport.js 的優勢在於提供超過 500 組驗證策略(Strategy) — 也就是驗證機制 — 的模組,讓我們可以依照自己的需求,使用相對應的驗證策略,來打造驗證機系統,也因此有人形容 Passport 相對 Lightweight!
使用 Passport 的重點部分
- 載入 Passport 模組,並初始化與使用模組中的兩個 middleware:
passport.initialize()
和passport.session()
- 建立「驗證策略」及「序列化與反序列化」的方法:
serializeUser()
和deserializeUser()
- 建立驗證所需路由,並使用
passport.authenticate()
middleware 驗證使用者 - 建立
ensureAuthenticated()
middleware,在路由上針對往後的請求都先行確認使用者是已經通過驗證的狀態 — Route Protection
使用 Passport 的成果
以下將針對上述所提到的幾個重點部分做詳細說明,若有興趣了解我過去幾個套用 Passport.js 的專案程式碼,可以點擊以下專案名稱:
👉 餐廳清單
👉 家庭記帳本
建立驗證路由
要請 Passport 針對來自使用者的請求展開驗證時,需先在登入的路由中,加上「passport.authenticate()
middleware」,並提供:
- 要採用哪一個驗證策略:在此使用本地驗證 — local(下方將詳細介紹)
- 驗證成功/失敗將導向哪個路徑:在此分別設定為首頁和登入的路徑
彈性的方式設計路由
第一種方式在驗證完成後,根據驗證結果,很簡單的將用戶重新導向相對應的路徑。然而,有些情況我們則是會想在驗證成功後,先執行某些動作,才將用戶導向指定的頁面,這時也可以將路由設計成:
透過passport.authenticate()
middleware 驗證使用者:
- 若驗證失敗:使用者將被直接導向於
failureRedirect
所指定的路徑,且不執行下一個 middleware - 若驗證成功:執行下一個 middleware,且該用戶的資訊將被放到
req.body
— 函式中能先執行一些額外的程式碼,才將用戶導向根路徑
👉路由的設計可以很彈性:官方文件上還有一些例子可以參考
建立驗證策略
設定策略:本地驗證
在建立完驗證路由後,需要建立「Strategy 驗證機制」 — 其中一個常見的驗證機制是本地驗證(LocalStrategy):「在 Node.js 應用程式中,是透過帳號及密碼完成驗證」
- 引入「passport 模組」及「驗證策略模組」
- 透過 passport 模組中
use()
方法建立驗證策略:在此使用 LocalStrategy
Verify Callback
在告知要使用哪一種驗證策略時,需要搭配一組 Verify Callback — 用途是未來一旦 Passport 要展開驗證時,Passport 將:
- 取得使用者的驗證資訊 — 使用本地驗證時,將從
req.body
中「帳號和密碼」的欄位取得使用者資訊來驗證 - 呼叫 Verify Callback 函式,並將帳號和密碼作為參數帶入展開驗證
- 完成驗證後,呼叫
done
並帶入驗證結果
使用其他欄位資訊驗證
使用「LocalStrategy 本地驗證」時,該策略預設是到req.body
中名為 username
和 password
的欄位取得帳號、密碼資訊,然而,也可改以其他名稱的欄位作為帳號和密碼來驗證:
設定方式是在本地策略中 Varify Callback 函式前,置入一個用於設定的 options
物件來做修改。以上範例是在物件中設定以下兩個屬性,並依據欄位名稱同時修改 Varify Callback 上的引數名稱:
usernameField
:改以名為email
的欄位資訊作為帳號passwordField
:改以名為passwd
的欄位資訊作為密碼
❗️補充 options 物件
1. options 物件只在需要額外做設定時使用,因此非必填
2. Syntax: new LocalStrategy({/* options */, callback}
在 Varify Callback 上使用 request 物件
透過 Varify Callback 驗證時,有時可能會需要用到 req
物件上的資訊,或新增、修改 req
物件上的資訊。例如:要針對不同的驗證結果設定相對應的 flash 時,需要取得 req
物件做 req.flash()
設定。
讓 Varify Callback 可以取得 req 物件使用的設定方式:
- 在
options
物件上設定passReqToCallback
屬性值為true
- 新增
req
引數到 Varify Callback 函數中
回傳驗證結果
done
也是一個 callback function,當驗證完成後,可以呼叫 done
並將驗證結果作為參數帶入,提供給 Passport 使用。done
接收三個參數:
- 錯誤訊息:例如在伺服器端回傳錯誤訊息時,帶入錯誤訊息
err
;無錯誤訊息時,則可以帶入null
取代 - 使用者資料:驗證成功時,帶入使用者資料
user
;驗證失敗時,則可以帶入false
取代 - 驗證失敗訊息:當驗證失敗時,可以額外補充驗證失敗的原因和資訊
❗️錯誤 vs 驗證失敗
👉錯誤是指在伺服器端回傳錯誤訊息,例如:向資料庫抓取資料失敗
👉驗證失敗代表伺服器正常運作,但驗證不成功,例如:帳號或密碼錯誤
設定完成後的本地驗證策略
截至目前為止的驗證流程
建立 Middlewares
建立完驗證策略後,需要在 Express 中設定相關的 Middleware — 除了載入和初始化常用到的 body-parser 和 express-session 兩個套件,須額外使用:
passport.initialize()
:初始化 passportpassport.session()
:有使用 login session 時,需設定這條 middleware
❗️session() 和 passport.session() 設定順序
session() 需設定在 passport.session() 之前,以確保 session 能正確地被處理
Session 運作
在 Passport 中使用 session 時,需要了解:
「只有在第一次登入時,完整的使用者登入資訊會隨著請求傳送到伺服器端來提供驗證,一但成功通過驗證,特定的用戶資訊將透過 session 被儲存起來,並將 cookie 回傳給使用者 — 未來透過使用者請求中的 cookie 就可以對應到此 session,找回所需要的使用者資訊」
passport.serializeUser() 方法
驗證成功並取得使用者資訊後,將經過一序列化的過程 — 此方法可以在獲得user
物件後,決定將user
物件裡的哪些片段資訊存到 login session 當中。在範例中,只將使用者user
物件中的 id 資訊序列化,存到 session 當中:
passport.serializeUser() 序列化的結果將被放到:
👉req.session.passport.user
👉req.user
passport.deserializeUser() 方法
未來 passport.deserializeUser()
在獲得存取在 session 當中的用戶資訊後,透過該資訊至資料庫找到完整的用戶資料物件,並將該物件存取到req.user
上。在範例中,透過使用者 id 到資料庫尋找完整的用戶資料:
驗證完成的後續請求過程
在成功驗證後,往後來自客戶端的請求將經過以下流程:
- 透過 cookie 上的 session id 至 session 中取得被序列化的用戶資訊,存放到
req.session.passport.user
passport.initialize()
被觸發:確認passport.user
上帶有被序列化的使用者物件,若物件不存在,則創建一個空物件passport.session()
被觸發:若有找到被序列化的使用者物件,則判定此請求的用戶是已經通過驗證的狀態passport.session()
呼叫deserializeUser()
方法— 透過用戶id
前往資料庫找到使用者完整資料,放置在req.user
上供往後使用
完整的驗證流程
Route Protection
在瀏覽許多網站時可以發現,要前往某些頁面或執行某些行為時是需要先經過登入(身份驗證)的步驟。例如:在購物商城購買商品前,需要先登入、在網路銀行也需要是登入的狀態,才能進行轉帳、查看帳戶等行為。
在路由的設計中,也能置入一個客製化的 Middleware — 先行確保使用者是通過驗證的狀態,才能執行後續的行為:「Route Protection」
req.isAuthenticated()
isAuthenticated()
是一個在 req
物件上可以使用的 passport 方法 — 來得知使用者是否已經通過驗證。
客製化 middleware
在以下範例中,借助req.isAuthenticated()
的功能,建立了一個客製化的 ensureAuthenticated()
middleware,其中req.isAuthenticated()
若回傳:
true
:代表使用者已驗證,呼叫next()
執行下一個 middlewarefalse
:代表使用者尚未驗證,將使用者導向登入頁面
在路由中使用客製化 middleware
先確認哪些路由需要受到保護 — 使用者需為通過驗證的狀態 — 再於路由中設置ensureAuthenticated()
middleware。在以下範例中的路由,使用者就需要為「已驗證」的狀態,才會渲染 new
頁面,否則將被導向登入頁面:
心得
透過 Passport 建立驗證機制是很實用和方便的,開發者可以依據驗證需求,載入相對應的驗證策略 — 除了本篇使用的「本地驗證」策略,還可以使用常見的第三驗證。
從接觸 Passport.js 到現,已經運用到數個專案中,也因此漸漸熟悉整個運作和使用。但回想當時初學時,也花了不少時間在翻找官方文件和查找相關資料,以了解運作的流程及實際上該如何使用 —畢竟每個專案的需求不同,想達到的效果也會有差異,但有些功能或設定未必能在官方文件中找尋的到,或需要花時間翻找一番才能找得到,因此以本篇作為筆記,記錄下過程中遇到的問題和解答!
總之,很好玩,也很開心能實際運用在不少專案上!
相關資源
- Understanding passport.js authentication flow
- A peep beneath the hood of PassportJS’ OAuth flow
- Understanding Sessions and Local Authentication in Express with Passport and MongoDb
- Passport.js: Documentation
- [Node] Passport 學習筆記(Learn to Use Passport JS)
- stackoverflow: req.isAuthenticated()
- stackoverflow: passport.session()