單頁應用架構的Web應用日益增多,這類應用將整個應用作為JavaScript加載到瀏覽器中,所有與服務器的交互都通過返回JSON文檔的基於HTTP的API進行。這些應用通常需要某種程度的用戶限制交互,例如存儲用戶個人資料詳細信息。在傳統的基於HTML的應用中實現這一功能相對簡單,但在需要對每個API請求進行身份驗證的單頁應用中,則更為棘手。
本文將演示一種使用Passport.js庫實現多種提供商的社交登錄,並由此實現後續API調用的基於令牌的身份驗證的技術。
本文的所有源代碼均可從我們的GitHub存儲庫下載。
關鍵要點
為什麼在您的SPA中使用社交登錄?
在您的Web應用中實現登錄機制時,需要考慮許多問題。
在開始編寫登錄門戶之前,需要考慮這些以及更多問題。但是,有一種更好的方法。
許多網站,主要是社交網絡,允許您使用它們的平台來驗證您自己的應用。這是通過許多不同的API實現的——OAuth 1.0、OAuth 2.0、OpenID、OpenID Connect等。
通過使用這些社交登錄技術來實現您的登錄流程,可以提供許多優勢。
為什麼對您的API使用基於令牌的身份驗證?
每當客戶端需要訪問您的API時,您都需要某種方法來確定它們是誰以及是否允許訪問。實現此目標的方法有很多,但主要選項是:
基於會話的身份驗證需要您的API服務能夠將會話與客戶端關聯起來。這通常非常容易設置,但是如果您在多個服務器上部署您的API,則可能會出現問題。您也受服務器用於會話管理和過期的機制的限制,這可能不受您的控制。
基於cookie的方法是,您只需在cookie中存儲一些標識符,這將用於自動識別API請求。這意味著您首先需要某種設置cookie的機制,並且您有可能會在後續請求中洩露它,因為cookie會自動包含在對同一主機的所有(合適的)請求中。
基於令牌的方法是基於cookie的身份驗證的一種變體,但它可以讓您更多地控制。本質上,您生成令牌的方式與基於cookie的身份驗證系統相同,但是您將自己包含在請求中——通常在“Authorization”標頭中或直接在URL中。這意味著您可以完全控制存儲令牌、哪些請求將包含它等等。
注意:即使HTTP標頭稱為“Authorization”,我們實際上也在使用它進行身份驗證。這是因為我們正在使用它來確定客戶端“是誰”,而不是客戶端“被允許做什麼”。
用於生成令牌的策略也很重要。這些令牌可以是引用令牌,這意味著它們只不過是服務器用來查找真實詳細信息的標識符。或者完整的令牌,這意味著令牌已經包含了所有必要的信息。
引用令牌具有顯著的安全優勢,因為絕對不會向客戶端洩露用戶憑據。但是,由於您需要在每次發出的請求中將令牌解析為實際憑據,因此會產生性能損失。
完整令牌則相反。它們會將用戶憑據暴露給任何能夠理解令牌的人,但是由於令牌是完整的,因此在查找它時不會產生性能損失。
通常,完整令牌將使用JSON Web Tokens標準來實現,因為該標准允許改進令牌的安全性。具體來說,JWT允許對令牌進行加密簽名,這意味著您可以保證令牌沒有被篡改。還規定可以對它們進行加密,這意味著如果沒有加密密鑰,甚至無法解碼令牌。
如果您想回顧一下在Node中使用JWT,請查看我們的教程:使用JSON Web Tokens與Node.js。
使用完整令牌的另一個缺點是大小。例如,引用令牌可以使用UUID來實現,其長度為36個字符。相反,JWT很容易長達數百個字符。
在本文中,我們將使用JWT令牌來演示它們如何工作。但是,當您自己實現此功能時,您需要決定是否要使用引用令牌或完整令牌,以及將為此使用什麼機制。
什麼是Passport?
Passport是Node.js的一組模塊,用於在您的Web應用中實現身份驗證。它可以非常輕鬆地插入許多基於Node的Web服務器,並使用模塊化結構來實現您需要的登錄機制,而不會產生過多的膨脹。
Passport是一個功能強大的模塊套件,涵蓋了大量的身份驗證需求。使用這些模塊,我們可以創建一個可插入的設置,允許為不同的端點提供不同的身份驗證需求。所使用的身份驗證系統可以像簡單地檢查URL中的特殊值一樣簡單,也可以像依賴第三方提供商來完成所有工作一樣複雜。
在本文中,我們將使用passport-google-oauth、passport-facebook和passport-jwt模塊,以便為API端點實現社交登錄和基於JWT令牌的身份驗證。
passport-jwt模塊將用於要求某些端點——我們實際需要身份驗證才能訪問的API端點——在請求中必須存在有效的JWT。 passport-google-oauth和passport-facebook模塊將用於提供分別針對Google和Facebook進行身份驗證的端點,然後生成可用於訪問應用中其他端點的JWT。
為您的單頁應用實現社交登錄
從這裡開始,我們將逐步介紹如何獲取一個簡單的單頁應用並為其實現社交登錄。此應用使用Express編寫,一個簡單的API提供一個安全端點和一個不安全端點。如果您想繼續操作,可以從https://github.com/sitepoint-editors/social-logins-spa檢出此應用的源代碼。可以通過在下載的源代碼中執行npm install來構建此應用——下載所有依賴項——然後通過執行node src/index.js來運行。
為了成功使用該應用,您需要在Google和Facebook註冊社交登錄憑據,並將憑據提供給該應用。完整的說明可在演示應用的README文件中找到。這些憑據作為環境變量訪問。因此,應用可以按如下方式運行:
# Linux / OS X $ export GOOGLE_CLIENTID=myGoogleClientId $ export GOOGLE_CLIENTSECRET=myGoogleClientSecret $ export FACEBOOK_CLIENTID=myFacebookClientId $ export FACEBOOK_CLIENTSECRET=myFacebookClientSecret $ node src/index.js
# Windows > set GOOGLE_CLIENTID=myGoogleClientId > set GOOGLE_CLIENTSECRET=myGoogleClientSecret > set FACEBOOK_CLIENTID=myFacebookClientId > set FACEBOOK_CLIENTSECRET=myFacebookClientSecret > node src/index.js
此過程的最終結果是將令牌身份驗證支持(使用JSON Web Tokens)添加到我們的安全端點,然後添加社交登錄支持(使用Google和Facebook)以獲取令牌供應用的其餘部分使用。這意味著您需要使用社交提供商進行一次身份驗證,然後使用生成的JWT進行應用中所有未來的API調用。
JWT對於我們的場景來說是一個特別好的選擇,因為它們是完全自包含的,同時仍然是安全的。 JWT由JSON有效負載和加密簽名組成。有效負載包含已認證用戶的詳細信息、認證系統和令牌的有效期。然後,簽名確保惡意第三方無法偽造它——只有擁有簽名密鑰的人才能生成令牌。
在閱讀本文時,您會經常看到對作為應用一部分包含的config.js模塊的引用。此模塊用於配置應用,並使用Node-convict模塊進行外部配置。本文中使用的配置如下:
在您的應用中使用Passport之前,需要進行少量設置。這只不過是確保模塊已安裝,並在您的Express應用中初始化中間件。
此階段所需的模塊是passport模塊,然後要設置中間件,我們只需將其添加到我們的Express應用中即可。
// src/index.js const passport = require('passport'); ..... app.use(passport.initialize());
如果您遵循Passport網站上的說明,則會讓您設置會話支持——通過使用passport.session()調用。我們在應用中不使用任何會話支持,因此這是不必要的。這是因為我們正在實現一個無狀態API,因此我們將對每個請求提供身份驗證,而不是將其持久化到會話中。
使用Passport設置JWT令牌身份驗證相對簡單。我們將使用passport-jwt模塊,它將為我們完成所有繁重的工作。此模塊查找值為“JWT ”開頭的“Authorization”標頭,並將標頭的其餘部分視為用於身份驗證的JWT令牌。然後,它解碼JWT並將其中存儲的值提供給您自己的代碼進行操作——例如,執行用戶查找。如果JWT無效,例如簽名無效、令牌已過期……則請求將未經身份驗證,而無需您自己的代碼額外參與。
然後,配置JWT令牌身份驗證的方法如下:
# Linux / OS X $ export GOOGLE_CLIENTID=myGoogleClientId $ export GOOGLE_CLIENTSECRET=myGoogleClientSecret $ export FACEBOOK_CLIENTID=myFacebookClientId $ export FACEBOOK_CLIENTSECRET=myFacebookClientSecret $ node src/index.js
在上面,我們使用了一些內部模塊:
在這裡,我們使用已知的密鑰、發行者和受眾配置JWT解碼器,並且我們告知策略它應該從Authorization標頭獲取JWT。如果發行者或受眾中的任何一個與JWT中存儲的內容不匹配,則身份驗證將失敗。這為我們提供了另一層防偽造保護,儘管這是一個非常簡單的保護。
令牌解碼完全由passport-jwt模塊處理,我們只需要提供與最初用於生成令牌的配置相對應的配置即可。因為JWT是一個標準,所以任何遵循該標準的模塊都能完美地協同工作。
成功解碼令牌後,它將作為有效負載傳遞給我們的回調。在這裡,我們只是嘗試查找由令牌中的“主題”標識的用戶。實際上,您可能會進行額外的檢查,例如確保令牌未被吊銷。
如果找到用戶,我們將其提供給Passport,然後Passport將其提供給請求處理的其餘部分作為req.user。如果找不到用戶,則我們不向Passport提供任何用戶,然後Passport將認為身份驗證失敗。
這現在可以連接到請求處理程序,以便請求需要身份驗證才能成功:
# Windows > set GOOGLE_CLIENTID=myGoogleClientId > set GOOGLE_CLIENTSECRET=myGoogleClientSecret > set FACEBOOK_CLIENTID=myFacebookClientId > set FACEBOOK_CLIENTSECRET=myFacebookClientSecret > node src/index.js
上面第3行是使Passport處理請求的魔法。這會導致Passport在我們傳入的請求上運行我們剛剛配置的“jwt”策略,並允許其繼續進行或立即失敗。
我們可以通過運行應用——通過執行node src/index.js——並嘗試訪問此資源來查看其運行情況:
// src/index.js const passport = require('passport'); ..... app.use(passport.initialize());
我們沒有提供任何Authorization標頭,它不允許我們繼續進行。但是,如果您提供有效的Authorization標頭,您將獲得成功的響應:
// src/authentication/jwt.js const passport = require('passport'); const passportJwt = require('passport-jwt'); const config = require('../config'); const users = require('../users'); const jwtOptions = { // 从 "Authorization" 标头获取 JWT。 // 默认情况下,这会查找 "JWT " 前缀 jwtFromRequest: passportJwt.ExtractJwt.fromAuthHeader(), // 用于签署 JWT 的密钥 secretOrKey: config.get('authentication.token.secret'), // JWT 中存储的发行者 issuer: config.get('authentication.token.issuer'), // JWT 中存储的受众 audience: config.get('authentication.token.audience') }; passport.use(new passportJwt.Strategy(jwtOptions, (payload, done) => { const user = users.getUserById(parseInt(payload.sub)); if (user) { return done(null, user, payload); } return done(); }));
為了執行此測試,我通過訪問https://www.jsonwebtoken.io並填寫那裡的表單手動生成了一個JWT。 “有效負載”是我使用的:
// src/index.js app.get('/api/secure', // 此请求必须使用 JWT 进行身份验证,否则我们将失败 passport.authenticate(['jwt'], { session: false }), (req, res) => { res.send('Secure response from ' + JSON.stringify(req.user)); } );
“簽名密鑰”是“mySuperSecretKey”,取自配置。
現在我們可以只使用有效令牌訪問資源了,我們需要一種實際生成令牌的方法。這是使用jsonwebtoken模塊完成的,它構建了一個包含正確詳細信息並使用與上面相同的密鑰簽名的JWT。
# Linux / OS X $ export GOOGLE_CLIENTID=myGoogleClientId $ export GOOGLE_CLIENTSECRET=myGoogleClientSecret $ export FACEBOOK_CLIENTID=myFacebookClientId $ export FACEBOOK_CLIENTSECRET=myFacebookClientSecret $ node src/index.js
請注意,我們在生成JWT時使用完全相同的受眾、發行者和密鑰配置設置。我們還指定JWT的有效期為一小時。這可以是您認為對您的應用來說合理的任何時間段,甚至可以從配置中提取,以便可以輕鬆更改。
在這種情況下,沒有指定JWT ID,但這可以用來為令牌生成一個完全唯一的ID——例如使用UUID。然後,這為您提供了一種吊銷令牌的方法,並在數據存儲中存儲吊銷ID的集合,並在處理Passport策略中的JWT時檢查JWT ID是否不在列表中。
現在我們有能力生成令牌了,我們需要一種讓用戶實際登錄的方法。這就是社交登錄提供商發揮作用的地方。我們將添加一項功能,讓用戶重定向到社交登錄提供商,並在成功後生成JWT令牌並將其提供給瀏覽器的JavaScript引擎以供將來請求使用。我們已經具備了幾乎所有這方面的組件,我們只需要將它們組合在一起即可。
Passport中的社交登錄提供商分為兩部分。首先,需要使用適當的插件實際為社交登錄提供商配置Passport。其次,需要用戶被定向到的Express路由才能啟動身份驗證,以及用戶在身份驗證成功後被重定向到的路由。
我們將在新的子瀏覽器窗口中打開這些URL,我們可以在完成後關閉這些窗口,並且能夠調用打開它的窗口內的JavaScript方法。這意味著該過程對於用戶來說相對透明——最多他們會看到一個新的窗口打開,要求他們提供憑據,但最好他們除了現在已登錄的事實外什麼也看不到。
此瀏覽器的方面需要由兩部分組成。彈出窗口的視圖以及在主窗口中處理此視圖的JavaScript。這可以很容易地與任何框架集成,但在此示例中,我們將為了簡單起見使用vanilla JavaScript。
主頁面JavaScript只需要類似這樣的內容:
# Windows > set GOOGLE_CLIENTID=myGoogleClientId > set GOOGLE_CLIENTSECRET=myGoogleClientSecret > set FACEBOOK_CLIENTID=myFacebookClientId > set FACEBOOK_CLIENTSECRET=myFacebookClientSecret > node src/index.js
這在窗口上註冊一個全局函數對象(名為authenticateCallback),它將存儲訪問令牌,然後打開我們的路由以啟動身份驗證,我們正在訪問/api/authentication/{provider}/start。
然後,可以使用您希望啟動身份驗證的任何方式觸發此函數。這通常是標題區域中的登錄鏈接,但詳細信息完全取決於您的應用。
第二部分是在成功身份驗證後要呈現的視圖。在這種情況下,我們為了簡單起見使用Mustache,但這將使用對您來說最合適的任何視圖技術。
# Linux / OS X $ export GOOGLE_CLIENTID=myGoogleClientId $ export GOOGLE_CLIENTSECRET=myGoogleClientSecret $ export FACEBOOK_CLIENTID=myFacebookClientId $ export FACEBOOK_CLIENTSECRET=myFacebookClientSecret $ node src/index.js
在這裡,我們只有一個簡單的JavaScript代碼,它在該窗口的打開程序(即主應用窗口)上調用上面的authenticateCallback方法,然後我們關閉自己。
此時,JWT令牌將在主應用窗口中可用,用於您想要的任何目的。
使用passport-google-oauth模塊將針對Google進行身份驗證。這需要提供三條信息:
客戶端ID和密鑰是通過在Google開發者控制台中註冊您的應用獲得的。重定向URL是用戶在使用其Google憑據登錄後將被發送回您的應用中的URL。這將取決於應用的部署方式和位置,但現在我們將對其進行硬編碼。
然後,我們的Google身份驗證Passport配置將如下所示:
# Windows > set GOOGLE_CLIENTID=myGoogleClientId > set GOOGLE_CLIENTSECRET=myGoogleClientSecret > set FACEBOOK_CLIENTID=myFacebookClientId > set FACEBOOK_CLIENTSECRET=myFacebookClientSecret > node src/index.js
當用戶在成功身份驗證後重定向回我們時,我們會得到他們在Google系統中的ID和一些個人資料信息。我們首先嘗試查看此用戶是否以前登錄過。如果是,那麼我們獲取他們的用戶記錄,我們就完成了。如果不是,我們將為他們註冊一個新帳戶,然後我們將使用這個新帳戶。這為我們提供了一種透明的機制,用戶註冊在第一次登錄時完成。如果需要,我們可以以不同的方式進行此操作,但現在沒有必要。
接下來是設置路由處理程序以管理此登錄。這些將如下所示:
// src/index.js const passport = require('passport'); ..... app.use(passport.initialize());
請注意/api/authentication/google/start和/api/authentication/gogle/redirect的路由。如上所述,/start變體是我們打開的URL,/redirect變體是Google在成功時將用戶重定向到的URL。然後,這將呈現我們上面顯示的已認證視圖,提供生成的JWT供其使用。
現在我們有了第一個社交登錄提供商,讓我們擴展並添加第二個。這次將是Facebook,使用passport-facebook模塊。
此模塊的工作方式與Google模塊幾乎相同,需要相同的配置和相同的設置。唯一的真正區別在於它是不同的模塊,並且訪問它的URL結構不同。
為了配置Facebook身份驗證,您還需要客戶端ID、客戶端密鑰和重定向URL。客戶端ID和客戶端密鑰(Facebook稱為應用ID和應用密鑰)可以通過在Facebook開發者控制台中創建Facebook應用獲得。您需要確保將“Facebook登錄”產品添加到您的應用中才能使其正常工作。
我們的Facebook身份驗證Passport配置將是:
// src/authentication/jwt.js const passport = require('passport'); const passportJwt = require('passport-jwt'); const config = require('../config'); const users = require('../users'); const jwtOptions = { // 从 "Authorization" 标头获取 JWT。 // 默认情况下,这会查找 "JWT " 前缀 jwtFromRequest: passportJwt.ExtractJwt.fromAuthHeader(), // 用于签署 JWT 的密钥 secretOrKey: config.get('authentication.token.secret'), // JWT 中存储的发行者 issuer: config.get('authentication.token.issuer'), // JWT 中存储的受众 audience: config.get('authentication.token.audience') }; passport.use(new passportJwt.Strategy(jwtOptions, (payload, done) => { const user = users.getUserById(parseInt(payload.sub)); if (user) { return done(null, user, payload); } return done(); }));
這與Google的配置幾乎相同,只是使用了“facebook”而不是“google”。 URL路由也類似:
// src/index.js app.get('/api/secure', // 此请求必须使用 JWT 进行身份验证,否则我们将失败 passport.authenticate(['jwt'], { session: false }), (req, res) => { res.send('Secure response from ' + JSON.stringify(req.user)); } );
在這裡,我們不需要指定我們要使用的範圍,因為默認集已經足夠好。否則,Google和Facebook之間的配置幾乎相同。
總結
使用社交登錄提供商可以快速輕鬆地將用戶登錄和註冊添加到您的應用中。事實上,這使用瀏覽器重定向將用戶發送到社交登錄提供商,然後發送回您的應用,這使得將其集成到單頁應用中可能會很棘手,即使將其集成到更傳統的應用中相對容易。
本文展示了一種將這些社交登錄提供商集成到單頁應用中的方法,這種方法有望既易於使用,又易於擴展到您可能希望使用的未來提供商。 Passport有很多模塊可以與不同的提供商一起工作,這只是找到合適的模塊並像我們上面對Google和Facebook所做的那樣對其進行配置的問題。
本文由James Kolce進行同行評審。感謝所有SitePoint的同行評審人員,使SitePoint內容達到最佳狀態
關於社交登錄集成的常見問題解答 (FAQs)
將社交登錄集成到您的Web應用中可以帶來多項好處。首先,它簡化了用戶的註冊流程,因為他們可以使用現有的社交媒體帳戶註冊,無需記住另一個用戶名和密碼。其次,它可以提高轉化率,因為簡化的註冊流程可以鼓勵更多用戶註冊。最後,它可以讓您訪問其社交媒體個人資料中的用戶數據,這些數據可用於個性化他們在您網站上的體驗。
在集成社交登錄時,確保用戶數據的安全至關重要。您可以通過使用安全的協議(如OAuth 2.0)進行身份驗證來實現這一點,這確保用戶密碼不會與您的應用共享。此外,您應該隻請求應用所需的最小數量的用戶數據,並確保安全地存儲這些數據。
是的,您可以在您的Web應用中集成多個社交登錄。這可以為用戶提供更多選擇,並增加他們註冊的可能性。但是,重要的是要確保無論用戶選擇使用哪個社交登錄,用戶體驗都能保持無縫銜接。
處理擁有多個社交媒體帳戶的用戶可能具有挑戰性。一種解決方案是允許用戶將多個社交媒體帳戶鏈接到您的應用上的單個帳戶。這樣,他們可以選擇使用任何已鏈接的帳戶登錄。
如果用戶停用其社交媒體帳戶,他們將無法再使用該帳戶登錄您的應用。為了處理這種情況,您可以為用戶提供添加電子郵件地址或電話號碼到其帳戶的選項,如果他們停用其社交媒體帳戶,則可以使用這些信息登錄。
可以使用CSS自定義社交登錄按鈕的外觀。但是,務必遵守社交媒體平台提供的品牌指南。例如,Facebook的“f”徽標應始終以其原始形式使用,並且不應以任何方式修改。
是的,社交登錄可用於Web和移動應用。在移動應用中集成社交登錄的過程與Web應用類似,但您可能需要使用社交媒體平台提供的特定SDK。
您可以通過在社交媒體平台上創建測試帳戶並使用這些帳戶登錄您的應用來測試社交登錄功能。這可以幫助您在應用啟動之前識別任何問題或錯誤。
如果用戶忘記使用哪個社交媒體帳戶註冊,您可以提供一個恢復選項,讓他們可以輸入其電子郵件地址或電話號碼以接收與其帳戶鏈接的社交媒體帳戶列表。
雖然可以使用某些工具和插件在不進行編碼的情況下集成社交登錄,但了解一些編碼知識會很有益。這可以為您提供更多靈活性和對集成過程的控制,還可以幫助您解決可能出現的任何問題。
以上是您的水療中心的社交登錄:通過Google和Facebook來驗證您的用戶的詳細內容。更多資訊請關注PHP中文網其他相關文章!