[筆記] 透過 Passport.js 實作驗證機制

Mike Huang
麥克的半路出家筆記
11 min readAug 6, 2019

Passport 介紹

Passport 用途

透過 Node.js 和 Express 在打造應用程式時,驗證系統是很常見的需求,這時 Passport 就是一套能發揮上的好工具。簡單來說,能把 Passport 想成是一個「處理驗證的 middleware」,因此能很容易的整合到 Express 當中 。

Strategy 的搭配

打造驗證系統時,一定會想到的問題是:「我應該要透過帳號和密碼的方式來打造,還是透過常見第三方驗證 — Facebook、Google、Twitter — 來打造驗證機制?」Passport.js 的優勢在於提供超過 500 組驗證策略(Strategy) — 也就是驗證機制 — 的模組,讓我們可以依照自己的需求,使用相對應的驗證策略,來打造驗證機系統,也因此有人形容 Passport 相對 Lightweight!

使用 Passport 的重點部分

  1. 載入 Passport 模組,並初始化與使用模組中的兩個 middleware:passport.initialize()passport.session()
  2. 建立「驗證策略」及「序列化與反序列化」的方法:serializeUser()deserializeUser()
  3. 建立驗證所需路由,並使用passport.authenticate() middleware 驗證使用者
  4. 建立 ensureAuthenticated() middleware,在路由上針對往後的請求都先行確認使用者是已經通過驗證的狀態 — Route Protection

使用 Passport 的成果

以下將針對上述所提到的幾個重點部分做詳細說明,若有興趣了解我過去幾個套用 Passport.js 的專案程式碼,可以點擊以下專案名稱:

👉 餐廳清單

👉 家庭記帳本

👉 Todo List

建立驗證路由

要請 Passport 針對來自使用者的請求展開驗證時,需先在登入的路由中,加上「passport.authenticate() middleware」,並提供:

  1. 要採用哪一個驗證策略:在此使用本地驗證 — local(下方將詳細介紹)
  2. 驗證成功/失敗將導向哪個路徑:在此分別設定為首頁和登入的路徑

彈性的方式設計路由

第一種方式在驗證完成後,根據驗證結果,很簡單的將用戶重新導向相對應的路徑。然而,有些情況我們則是會想在驗證成功後,先執行某些動作,才將用戶導向指定的頁面,這時也可以將路由設計成:

透過passport.authenticate() middleware 驗證使用者:

  • 若驗證失敗:使用者將被直接導向於failureRedirect所指定的路徑,且不執行下一個 middleware
  • 若驗證成功:執行下一個 middleware,且該用戶的資訊將被放到 req.body— 函式中能先執行一些額外的程式碼,才將用戶導向根路徑
👉路由的設計可以很彈性官方文件上還有一些例子可以參考

建立驗證策略

設定策略:本地驗證

在建立完驗證路由後,需要建立「Strategy 驗證機制」 — 其中一個常見的驗證機制是本地驗證(LocalStrategy):「在 Node.js 應用程式中,是透過帳號及密碼完成驗證」

建立驗證機制
  1. 引入「passport 模組」及「驗證策略模組」
  2. 透過 passport 模組中 use() 方法建立驗證策略:在此使用 LocalStrategy

Verify Callback

在告知要使用哪一種驗證策略時,需要搭配一組 Verify Callback — 用途是未來一旦 Passport 要展開驗證時,Passport 將:

  1. 取得使用者的驗證資訊 — 使用本地驗證時,將從req.body中「帳號和密碼」的欄位取得使用者資訊來驗證
  2. 呼叫 Verify Callback 函式,並將帳號和密碼作為參數帶入展開驗證
  3. 完成驗證後,呼叫 done 並帶入驗證結果

使用其他欄位資訊驗證

使用「LocalStrategy 本地驗證」時,該策略預設是到req.body中名為 usernamepassword 的欄位取得帳號、密碼資訊,然而,也可改以其他名稱的欄位作為帳號和密碼來驗證:

設定方式是在本地策略中 Varify Callback 函式前,置入一個用於設定的 options 物件來做修改。以上範例是在物件中設定以下兩個屬性,並依據欄位名稱同時修改 Varify Callback 上的引數名稱:

  1. usernameField:改以名為 email 的欄位資訊作為帳號
  2. 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 物件使用的設定方式:

  1. options 物件上設定 passReqToCallback 屬性值為 true
  2. 新增 req 引數到 Varify Callback 函數中

回傳驗證結果

done 也是一個 callback function,當驗證完成後,可以呼叫 done 並將驗證結果作為參數帶入,提供給 Passport 使用。done接收三個參數:

  1. 錯誤訊息:例如在伺服器端回傳錯誤訊息時,帶入錯誤訊息 err;無錯誤訊息時,則可以帶入null取代
  2. 使用者資料:驗證成功時,帶入使用者資料user;驗證失敗時,則可以帶入false取代
  3. 驗證失敗訊息:當驗證失敗時,可以額外補充驗證失敗的原因和資訊
❗️錯誤 vs 驗證失敗
👉錯誤是指在伺服器端回傳錯誤訊息,例如:向資料庫抓取資料失敗
👉驗證失敗代表伺服器正常運作,但驗證不成功,例如:帳號或密碼錯誤

設定完成後的本地驗證策略

截至目前為止的驗證流程

建立 Middlewares

建立完驗證策略後,需要在 Express 中設定相關的 Middleware — 除了載入和初始化常用到的 body-parser 和 express-session 兩個套件,須額外使用:

  1. passport.initialize():初始化 passport
  2. passport.session():有使用 login session 時,需設定這條 middleware
❗️session() 和 passport.session() 設定順序
session() 需設定在 passport.session() 之前,以確保 session 能正確地被處理
在 app.js 中初始化相關套件和設定

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 到資料庫尋找完整的用戶資料:

驗證完成的後續請求過程

在成功驗證後,往後來自客戶端的請求將經過以下流程:

  1. 透過 cookie 上的 session id 至 session 中取得被序列化的用戶資訊,存放到 req.session.passport.user
  2. passport.initialize() 被觸發:確認 passport.user 上帶有被序列化的使用者物件,若物件不存在,則創建一個空物件
  3. passport.session() 被觸發:若有找到被序列化的使用者物件,則判定此請求的用戶是已經通過驗證的狀態
  4. 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() 執行下一個 middleware
  • false:代表使用者尚未驗證,將使用者導向登入頁面

在路由中使用客製化 middleware

先確認哪些路由需要受到保護 — 使用者需為通過驗證的狀態 — 再於路由中設置ensureAuthenticated() middleware。在以下範例中的路由,使用者就需要為「已驗證」的狀態,才會渲染 new 頁面,否則將被導向登入頁面:

心得

透過 Passport 建立驗證機制是很實用和方便的,開發者可以依據驗證需求,載入相對應的驗證策略 — 除了本篇使用的「本地驗證」策略,還可以使用常見的第三驗證。

從接觸 Passport.js 到現,已經運用到數個專案中,也因此漸漸熟悉整個運作和使用。但回想當時初學時,也花了不少時間在翻找官方文件和查找相關資料,以了解運作的流程及實際上該如何使用 —畢竟每個專案的需求不同,想達到的效果也會有差異,但有些功能或設定未必能在官方文件中找尋的到,或需要花時間翻找一番才能找得到,因此以本篇作為筆記,記錄下過程中遇到的問題和解答!

總之,很好玩,也很開心能實際運用在不少專案上!

--

--

Mike Huang
麥克的半路出家筆記

熱愛接觸和學習網頁開發相關技術與知識、喜歡分享、旅遊和咖啡的軟體工程師 A software engineer who enjoy learning and sharing web technologies & fancy coffee and travelling