Overview 概覽

Passport 是一個 Node 平臺的身份認證中間件。它的設計服務於一個簡單的目標:身份認證請求。編寫模塊時,封裝是一個美德,所以 Passport 把所有其他功能交給應用自身去做。這種關注點的分離可以保持代碼整潔並且可維護,同時使得 Passport 極其容易地集成到應用中。

在現代 web 應用中,身份認證可以採用多種形式。傳統而言,用戶通過提供用戶名和密碼登錄。由於社交網路的興起,使用類似於 Facebook 或者 Twitter 的 OAuth 提供商進行單點登錄(single sign-on)已經成為一個流行的身份驗證方式。暴露 API 的服務通常需要基於 token 的憑證來保護訪問行為。

Passport 認為每一個應用都有獨一無二的身份認證需求。身份認證機制,被稱為策略(strategies),打包成獨立的模塊。應用可以選擇應用哪些策略,同時避免創建不必要的依賴。

儘管身份驗證過程很複雜,代碼也可以很簡潔。

app.post(/login, passport.authenticate(local,{ successRedirect: /, failureRedirect: /login}));

Authenticate 身份認證

認證請求很簡單,只需要調用 passport.authenticate() 並且指定要應用的策略。authenticate 的函數簽名是一個標準的 Connect 中間件,因此可以方便地作為 Express 應用中的路由中間件。

app.post(/login, passport.authenticate(local), function(req, res) { // 如果這個函數被調用了,說明認證成功。 // `req.user` 包含已認證的用戶 res.redirect(/users/ + req.user.username);});

默認情況下,如果認證失敗,Passport 將響應一個 401 Unauthorized 狀態,並且任何額外的路由處理器都不會被調用。如果認證成功,下一個處理器會被調用,同時 req.user 屬性將被設置為已認證的用戶。

注意:在路由中使用之前,策略必須被提前設置好。

Redirects 重定向

認證一個請求後通常會發出一個重定向。

app.post(/login, passport.authenticate(local, { successRedirect: /, failureRedirect: /login}));

在這種情況下,重定向選項覆蓋了默認的行為。一旦認證成功,用戶將被重定向到首頁。如果認證失敗,用戶將被重定向回登陸頁進行重試。

Flash Messages 快閃消息

重定向經常和快閃消息 (Flash Messages) 結合,用來展示向用戶展示狀態信息。

app.post(/login, passport.authenticate(local, { successRedirect: /, failureRedirect: /login, failureFlash: true }));

設置 failureFlash 選項為 true 通知 Passport 快速展示一個error 消息,如果有的話,策略驗證回調函數提供的消息。這通常是最佳的方式,因為驗證回調函數可以最準確地確認為何認證失敗。

另外,快閃消息可以被顯式地設置。

passport.authenticate(local, { failureFlash: Invalid username or password. });

successFlesh 選項可以用來在認證成功時顯示一個 success 消息。

passport.authenticate(local, { successFlash: Welcome! });

Disable Sessions 禁用會話

成功認證之後,Passport 將建立一個持續的登錄會話。這在用戶通過瀏覽器訪問一個 web 應用的普遍場景中是有用的。然而,在某些情況下,會話支持是不必要的。例如,API 服務通常需要每一個請求都提供一個憑證。在這種情況下,會話支持可以通過設置 session 選項為 fasle 來被安全地禁用。

app.get(/api/users/me, passport.authenticate(basic, { session: false }), function(req, res) { res.json({ id: req.user.id, username: req.user.username });});

Custom Callback 自定義回調

如果內置的選項不足以處理身份認證請求,可以提供一個自定義的回調函數來允許應用自己處理成功或者失敗。

app.get(/login, function(req, res, next) { passport.authenticate(local, function(err, user, info) { if (err) { return next(err); } if (!user) { return res.redirect(/login); } req.logIn(user, function(err) { if (err) { return next(err); } return res.redirect(/users/ + user.username); }); })(req, res, next);});

在這個例子中,注意 authenticate 實在路由處理器內部被調用的,而不是作為一個路由中間件。通過閉包來使回調函數訪問到 reqres 對象。

如果身份認證失敗,user 將被設置為 false 。如果一個異常發生,err 將被設置。一個可選的 info 參數將被傳入,包含這個策略的驗證回調函數提供的其他細節。

回調函數可以根據需要使用提供的參數來處理認證的結果。注意在使用自定義回調函數時,應用需要負責建立回話(通過調用 req.login())和發送響應。

Configure 配置

使用 Passport 進行身份驗證,有三個部分需要設置:

  1. 認證策略
  2. 應用中間件
  3. 會話(可選)

Strategies 策略

Passport 是使用所謂策略 (strategies) 來對請求進行身份認證。策略的範圍包括驗證用戶名和密碼,使用 OAuth 的委託認證,或者使用 OpenID 的聯合認證。

在請求 Passport 對一個請求進行身份認證前,必需配置應用所使用的策略。

策略和他們的配置,通過 use 函數提供。例如,下面為用戶名/密碼認證使用了 LocalStrategy

app.get(/login, function(req, res, next) { passport.authenticate(local, function(err, user, info) { if (err) { return next(err); } if (!user) { return res.redirect(/login); } req.logIn(user, function(err) { if (err) { return next(err); } return res.redirect(/users/ + user.username); }); })(req, res, next);});

Verify Callback 驗證回調函數

這個例子介紹了一個重要的概念。策略需要所謂驗證回調函數。驗證回調函數的目的是找到擁有一組憑證的用戶。

當 Passport 對一個請求進行身份認證時,它解析包含在請求中的憑證。然後將這些憑證作為參數調用驗證回調函數,在這個例子中是 usernamepassword 。如果憑證有效,驗證回調調用 done 來把這個認證了的用戶提供給 Passport。

return done(null, user);

如果憑證無效(例如,密碼不正確),應該在調用 done 時傳入 false 而不是一個用戶來告訴 Passport 一個身份認證失敗。

return done(null, false);

可以提供一個額外的信息消息來說明失敗的原因。這有助於顯示一個快閃消息提示用戶重試時。

return done(null, false, { message: Incorrect password. });

最後,在驗證憑證時,如果發生一個異常(例如,資料庫不可用),應該按照常規的 Node 風格,在調用 done 時傳入一個錯誤。

return done(err);

注意區分這兩種失敗情況是很重要。後者是一個伺服器異常,err 是一個非 null 值。身份認證失敗是自然的情況,伺服器運行正常。確保 err 保持 null ,同時使用最後的參數來傳遞額外的細節。

通過這種方式的委託,驗證回調函數保持 Passport 獨立於資料庫。身份認證層不會強加任何前提條件,應用可以自由地選擇如何存儲用戶信息。

Middleware 中間件

在一個基於 Connect 或 Express 的應用中,需要使用 passport.initailize() 中間件來初始化 Passport。如果你的應用使用持續的登錄會話,passport.sesson() 中間件也必須使用。

app.configure(function() { app.use(express.static(public)); app.use(express.cookieParser()); app.use(express.bodyParser()); app.use(express.session({ secret: keyboard cat })); app.use(passport.initialize()); app.use(passport.session()); app.use(app.router);});

注意啟用會話支持是完全可選的,雖然在大多數應用中推薦使用它。如果啟用,確保在 passport.session 之前使用 session() 來保證登錄回話按照正確的順序保存。

在 Express 4.x 中 Connect 中間件不在包含在 Express 核心中,app.configure() 方法被移除。相同的中間件可以在與其等價的 npm 模塊中找到。

var session = require("express-session"), bodyParser = require("body-parser");app.use(express.static("public"));app.use(session({ secret: "cats" }));app.use(bodyParser.urlencoded({ extended: false }));app.use(passport.initialize());app.use(passport.session());

Sessions 會話

在一個典型 web 應用中,用來認證一個用戶的憑證只在登錄請求時傳遞。如果登錄成功,一個會話將被建立同時通過用戶瀏覽器的一個 cookie 設置來維護。

隨後的每一個請求將不再包含憑證,但是帶有標識回話的唯一 cookie。為了支持登錄回話,Passport 會將user 實例序列化到回話並從回話中反序列化 user 實例 。

passport.serializeUser(function(user, done) { done(null, user.id);});passport.deserializeUser(function(id, done) { User.findById(id, function(err, user) { done(err, user); });});

在這個例子中,只有用戶的 ID 被序列化到會話中,保持了會話中存儲最少的數據。接受到隨後的請求時,這個 ID 被用來查找用戶,用戶將被存儲到 req.user

這個序列化和反序列化的邏輯是由應用提供的,允許應用不受身份認證的限制,選擇一個合適的資料庫或對象映射器。

Username & Password 用戶和密碼

在網站中使用範圍最廣的用戶身份認證方式是通過一個用戶名和密碼。對這種機制的支持由 passport-local 模塊提供。

Configuration 配置

var passport = require(passport) , LocalStrategy = require(passport-local).Strategy;passport.use(new LocalStrategy( function(username, password, done) { User.findOne({ username: username }, function(err, user) { if (err) { return done(err); } if (!user) { return done(null, false, { message: Incorrect username. }); } if (!user.validPassword(password)) { return done(null, false, { message: Incorrect password. }); } return done(null, user); }); }));

本地身份認證的驗證回調接受 usernamepassword 參數,通過登錄表單提交給應用。

Form 表單

表單放置在 web 頁面上,允許用戶輸入他們的憑證然後登錄。

<form action="/login" method="post"> <div> <label>Username:</label> <input type="text" name="username"/> </div> <div> <label>Password:</label> <input type="password" name="password"/> </div> <div> <input type="submit" value="Log In"/> </div></form>

Route 路由

登錄表單通過 POST 提交給伺服器。使用 authenticate()local 策略可以處理這個登錄請求。

app.post(/login, passport.authenticate(local, { successRedirect: /, failureRedirect: /login, failureFlash: true }));

設置 failureFlash 選項為 true 命令 Passport 使用上面的驗證回調設置的 message 選項來快速展示一個 error 消息。這有助於提示用戶重試。

Parameters 參數

默認地,LocalStrategy 期望使用名為 usernamepassword 的參數中查找憑證。如果你的網站想要給這些欄位重新命名,可以通過選項來改變默認值。

passport.use(new LocalStrategy({ usernameField: email, passwordField: passwd }, function(username, password, done) { // ... }));

OpenID

OpenID 是一個聯合認證的開放標準。當訪問一個網站時,用於提交他們的 OpenID 來登錄。然後用戶使用他們選擇的 OpenID 提供者進行身份認證,提供者發出一個聲明來確認這個用戶的身份。網站驗證這個聲明來登記用戶。

對 OpenID 的支持由 passport-openid 模塊提供。

Configuration 配置

使用 OpenID 時,需要指定一個返回 URL 和範圍。returnURL 是用戶在他們的 OpenID 提供者認證後重定向的目標。realm 指示了身份認證生效的 URL 空間部分。通常它是網站的根 URL。

var passport = require(passport) , OpenIDStrategy = require(passport-openid).Strategy;passport.use(new OpenIDStrategy({ returnURL: http://www.example.com/auth/openid/return, realm: http://www.example.com/ }, function(identifier, done) { User.findOrCreate({ openId: identifier }, function(err, user) { done(err, user); }); }));

OpenID 認證的驗證回調函數接受一個 identifier 參數,包含了用戶的身份標識。

Form 表單

表單位於 web 頁面,允許用戶輸入他們的 OpenID 然後登錄。

<form action="/auth/openid" method="post"> <div> <label>OpenID:</label> <input type="text" name="openid_identifier"/><br/> </div> <div> <input type="submit" value="Sign In"/> </div></form>

Routes 路由

OpenID 認證需要兩個路由。第一個路由接受表單提交,包含 OpenID 身份標識。在認證過程中,用戶將被重定向到他們的 OpenID 提供商。第二個路由是用戶與 OpenID 提供商認證後返回的 URL。

// 接受 OpenID 身份標識,然後將用戶重定向到他們的 OpenID 提供商進行認證。// 完成後,提供商會把用戶定向到應用的地址:// /auth/openid/returnapp.post(/auth/openid, passport.authenticate(openid));// OpenID 提供商已經將用戶重定向到應用。通過校驗這個聲明來完成認證過程。// 如果有效,這個用戶將會登錄。否則,認證失敗。app.get(/auth/openid/return, passport.authenticate(openid, { successRedirect: /, failureRedirect: /login }));

Profile Exchange

可以選擇性地配置 OpenID 來檢索被認證用戶信息。通過設置 profile 選項為 true 來啟用用戶信息交換。

passport.use(new OpenIDStrategy({ returnURL: http://www.example.com/auth/openid/return, realm: http://www.example.com/, profile: true }, function(identifier, profile, done) { // ... }));

用戶信息交換被啟用時,驗證回調函數的簽名接受一個額外的 profile 參數,包含 OpenID 提供商提供的用戶信息。

OAuth

OAuth 是一個允許用戶授權 web 和桌面或移動應用進行 API 訪問的標準協議。一旦訪問被批准,被授權應用可以以這個用戶的名義使用 API。OAuth 也成為了一個流行的委託認證機制。

OAuth 有兩種風格,都被廣泛部署。

初始版本的 OAuth 作為一個開放標準,由一個鬆散的 web 開發者組織開發。他們的工作最終成為 OAuth 1.0 ,後來被 OAuth 1.0a 替代。這項工作成果已被 IETF 標準化為 RFC 5849。

由 Web Authorization Protocol Working Group 接手的新的工作計劃,已經致力於定義 OAuth 2.0 。鑒於漫長的標準化工作計劃,服務提供者已經開始部署符合多個草案的實現,包含了輕微的語義差別。

幸好,Passport 使應用規避了處理 OAuth 變體的複雜性。在很多情況下,可以使用一個提供者相關的策略,而不是上述的通用 OAuth 策略。這減少了一些必要設置,並能快速適配任何提供商特定的怪異模式。

對 OAuth 的支持由 passport-oauth 模塊提供。

OAuth 1.0

OAuth 1.0 是一個包含了多個步驟的委託認證策略。第一步,獲取一個 request token。然後,用戶被重定向到服務提供商進行訪問授權。最後,授權被批准後,用戶被定向回應用,同時可以用 request token 換取 access token。請求訪問的應用,被稱為消費者(consumer),通過一個 consumer key 和 consumer secret 標識。

Configuration 配置

使用通用的 OAuth 策略時,key, secret 和終端通過選項指定。

var passport = require(passport) , OAuthStrategy = require(passport-oauth).OAuthStrategy;passport.use(provider, new OAuthStrategy({ requestTokenURL: https://www.provider.com/oauth/request_token, accessTokenURL: https://www.provider.com/oauth/access_token, userAuthorizationURL: https://www.provider.com/oauth/authorize, consumerKey: 123-456-789, consumerSecret: shhh-its-a-secret callbackURL: https://www.example.com/auth/provider/callback }, function(token, tokenSecret, profile, done) { User.findOrCreate(..., function(err, user) { done(err, user); }); }));

基於 OAuth 的策略的驗證回調函數接受 token , tokenSecretprofile 參數。token 是 access token ,tokenSecret 是它對應的 secret。profile 將包含服務提供商提供的用戶信息。

Routes 路由

OAuth 認證需要兩個路由。第一個路由創建一個 OAuth 事務並將用戶定向到服務提供商。第二個路由是用戶與提供商認證後返回的 URL。

// 將用戶重定向到 OAuth 提供商進行認證。完成後,提供商將用戶定向回應用的地址:// /auth/provider/callbackapp.get(/auth/provider, passport.authenticate(provider));// OAuth 提供商已經將用戶重定向回應用。// 試圖獲取 access token 來完成認證。如果授權被許可,用戶將登錄。否則,認證失敗app.get(/auth/provider/callback, passport.authenticate(provider, { successRedirect: /, failureRedirect: /login }));

Link 鏈接

鏈接或者按鈕可以放置在 web 頁面中,點擊即開始認證過程。

<a href="/auth/provider">Log In with OAuth Provider</a>

OAuth 2.0

OAuth 2.0 是 OAuth 1.0 的繼任者,為解決早期版本中的一些已知的缺陷而設計。認證的過程基本相同。用戶先被重定向到服務提供商進行訪問授權。授權被許可後,用戶被重定向回到應用,攜帶一個用於交換 access token 的 code。請求訪問的應用,被稱為 client,通過 ID 和 secret 標識。

var passport = require(passport) , OAuth2Strategy = require(passport-oauth).OAuth2Strategy;passport.use(provider, new OAuth2Strategy({ authorizationURL: https://www.provider.com/oauth2/authorize, tokenURL: https://www.provider.com/oauth2/token, clientID: 123-456-789, clientSecret: shhh-its-a-secret callbackURL: https://www.example.com/auth/provider/callback }, function(accessToken, refreshToken, profile, done) { User.findOrCreate(..., function(err, user) { done(err, user); }); }));

基於 OAuth 2.0 的策略的驗證回調函數接受 accessTokenrefreshTOkenprofile 參數。refreshToken 可以用來獲取新的 access token,如果提供商沒有發送 refresh token 時可能為 undefinedprofile 將包含服務提供商提供的用戶信息。

Routes 路由

OAuth 2.0 認證需要兩個路由。第一個路由將用戶重定向到服務提供商。第二個路由是用戶與提供商認證結束後重定向的 URL。

// 將用戶重定向到 OAuth2.0 提供商進行認證。// 完成後,提供商將用戶重定向回到應用的地址:// /auth/provider/callbackapp.get(/auth/provider, passport.authenticate(provider));// OAuth 2.0 提供商已經將用戶重定向回到應用。// 嘗試獲取 access token 來完成認證過程。如果授權被許可,用戶將成功登錄。否則,認證失敗。app.get(/auth/provider/callback, passport.authenticate(provider, { successRedirect: /, failureRedirect: /login }));

Scope 作用域

使用 OAuth 2.0 請求訪問時,反問的作用域通過 scope 選項控制。

app.get(/auth/provider, passport.authenticate(provider, { scope: email }));Multiple scopes can be specified as an array.app.get(/auth/provider, passport.authenticate(provider, { scope: [email, sms] }));

scope 選項的值是提供商相關的。查閱提供商的文檔來獲取關於所支持的作用域的細節。

Link 鏈接

鏈接或者按鈕可以放置到 web 頁面中,點擊即開始認證過程。

<a href="/auth/provider">Log In with OAuth 2.0 Provider</a>

User Profile 用戶信息

當使用類似於 Facebook 或 Twitter 的第三方服務進行認證時,用戶信息通常是可以訪問的。每一個服務往往使用一個不一樣的方式來編碼這個信息。為了方便集成,Passport 最大程度地標準化了用戶信息。

標準化後的用戶信息遵循 Joseph Smarr 定製的 contact schema 。可用的通用欄位可以概括為下表。

provider {String} 用戶認證的提供商 (facebook, twitter, etc.)

id {String} 一個唯一的用戶標識,由服務提供商生成。

displayName {String} 這個用戶適合展示的名字。

name {Object}

  • familyName {String} 這個用戶的姓
  • givenName {String} 這個用戶的名
  • middleName {String} 這個用戶的中間名

emails {Array} [n]

  • value {String} 實際的 email 地址
  • type {String} email 地址的類型(home, work, etc.)

photos {Array} [n]

  • value {String} 圖片的 URL

注意所有上述欄位不是對每一個服務提供商都可用。有些提供商可能包含額外的沒有在這裡描述的欄位。查閱服務商相關的文檔獲取更多細節。

推薦閱讀:

相關文章