如何在不發瘋的情況下將 OAuth 2 集成到 Django/DRF 後端
已發表: 2022-03-11我們都去過那裡。 您正在處理 API 後端,並且對它的進展感到滿意。 您最近完成了最小可行產品 (MVP),測試全部通過,您期待實現一些新功能。
然後老闆給你發了一封郵件:“順便說一句,我們需要讓人們通過 Facebook 和 Google 登錄; 他們不應該只為像我們這樣的小網站創建一個帳戶。”
偉大的。 範圍蠕變再次來襲。
好消息是 OAuth 2 已成為社交和第三方身份驗證的行業標準(由 Facebook、Google 等服務使用),因此您可以專注於理解和實施該標準以支持廣泛的社交身份驗證提供者。
您可能不熟悉 OAuth 2; 當這發生在我身上時,我不是。
作為一名 Python 開發人員,您的直覺可能會導致您選擇 pip,這是 Python 包索引 (PyPA) 推薦的用於安裝 Python 包的工具。 壞消息是 pip 知道大約 278 個處理 OAuth 的包——其中 53 個特別提到了 Django。 僅僅研究選項就需要一周的工作,更不用說開始編寫代碼了。
在本教程中,您將學習如何使用 Python Social Auth 將 OAuth 2 集成到您的 Django 或 Django Rest Framework 中。 雖然本文重點介紹 Django REST 框架,但您可以應用此處提供的信息在各種其他常見後端框架中實現相同的功能。
OAuth 2 流程的快速概覽
OAuth 2 從一開始就被設計為一種 Web 身份驗證協議。 這與它被設計為網絡身份驗證協議並不完全相同。 它假定您可以使用 HTML 呈現和瀏覽器重定向等工具。
這顯然是基於 JSON 的 API 的障礙,但您可以解決這個問題。
您將像編寫傳統的服務器端網站一樣完成整個過程。
服務器端 OAuth 2 流程
第一步完全發生在應用程序流程之外。 項目所有者必須向您需要登錄的每個 OAuth 2 提供者註冊您的應用程序。
在此註冊期間,他們向 OAuth 2 提供者提供回調 URI ,您的應用程序將可在該回調 URI 處接收請求。 作為交換,他們會收到一個客戶端密鑰和客戶端密鑰。 在身份驗證過程中交換這些令牌以驗證登錄請求。
令牌將您的服務器代碼稱為客戶端。 主機是 OAuth 2 提供者。 它們不適用於您的 API 客戶端。
當您的應用程序生成一個包含按鈕的頁面時,流程就開始了,例如“使用 Facebook 登錄”或“使用 Google+ 登錄”。 從根本上說,這些只是簡單的鏈接,每個鏈接都指向一個如下所示的 URL:
https://oauth2provider.com/auth? response_type=code& client_id=CLIENT_KEY& redirect_uri=CALLBACK_URI& scope=profile& scope=email
(注意:為了便於閱讀,在上面的 URI 中插入了換行符。)
您已經提供了您的客戶端密鑰和重定向 URI,但沒有提供任何秘密。 作為交換,您已經告訴服務器您需要一個身份驗證代碼來響應並訪問“個人資料”和“電子郵件”範圍。 這些範圍定義了您向用戶請求的權限,並限制您收到的訪問令牌的授權。
收到後,用戶的瀏覽器會被定向到 OAuth 2 提供者控制的動態頁面。 OAuth 2 提供者在繼續之前驗證回調 URI 和客戶端密鑰是否相互匹配。 如果他們這樣做了,則流程會根據用戶的會話令牌短暫分歧。
如果用戶當前未登錄該服務,系統將提示他們這樣做。 一旦他們登錄,用戶就會看到一個對話框,請求允許您的應用程序登錄。
假設用戶批准,OAuth 2 服務器然後將它們重定向回您提供的回調 URI,包括查詢參數中的授權代碼: GET https://api.yourapp.com/oauth2/callback/?code=AUTH_CODE
。
授權碼是一種快速過期的一次性令牌; 收到它後,您的服務器應立即轉身,並向 OAuth 2 提供者發出另一個請求,包括授權代碼和您的客戶端密碼:
POST https://oauth2provider.com/token/? grant_type=authorization_code& code=AUTH_CODE& redirect_uri=CALLBACK_URI& client_id=CLIENT_KEY& client_secret=CLIENT_SECRET
此授權碼的目的是驗證上面的 POST 請求,但由於流程的性質,它必須通過用戶的系統進行路由。 因此,它本質上是不安全的。
對授權代碼的限制(即它很快過期並且只能使用一次)是為了減輕通過不受信任的系統傳遞身份驗證證書的固有風險。
此調用直接從您的服務器發送到 OAuth 2 提供者的服務器,是 OAuth 2 服務器端登錄過程的關鍵組件。 控制呼叫意味著您知道呼叫是受 TLS 保護的,從而有助於保護它免受竊聽攻擊。
包含授權代碼可確保用戶明確同意。 包括您的用戶永遠不可見的客戶端密碼,可確保此請求不是來自用戶系統上的某些病毒或惡意軟件,這些病毒或惡意軟件截獲了授權代碼。
如果一切都匹配,服務器會返回一個訪問令牌,您可以在以用戶身份進行身份驗證時調用該提供程序。
一旦您從服務器接收到訪問令牌,您的服務器就會再次將用戶的瀏覽器重定向到剛剛登錄的用戶的登錄頁面。將訪問令牌保留在用戶的服務器端會話緩存中是很常見的,所以服務器可以在必要時調用給定的社交提供者。
訪問令牌不應該提供給用戶!
我們可以深入了解更多細節。
例如,Google 包含一個刷新令牌,可以延長您的訪問令牌的生命週期,而 Facebook 提供一個端點,您可以在該端點上將短期訪問令牌交換為長期存在的訪問令牌。 不過,這些細節對我們來說並不重要,因為我們不會使用這個流程。
這個流程對於 REST API 來說很麻煩。 雖然您可以讓前端客戶端生成初始登錄頁面並讓後端提供回調 URL,但您最終會遇到問題。 收到訪問令牌後,您希望將用戶重定向到前端的登錄頁面,但沒有明確的 RESTful 方式可以做到這一點。
幸運的是,還有另一個可用的 OAuth 2 流程,在這種情況下效果更好。
客戶端 OAuth 2 流程
在此流程中,前端負責處理整個 OAuth 2 流程。 它通常類似於服務器端流程,但有一個重要的例外——前端位於用戶控制的機器上,因此不能委託他們掌握客戶端機密。 解決方案是簡單地消除該過程的整個步驟。
與服務器端流程一樣,第一步是註冊應用程序。
在這種情況下,項目所有者仍然註冊應用程序,但註冊為 Web 應用程序。 OAuth 2 提供者仍將提供客戶端密鑰,但可能不提供任何客戶端密鑰。
前端為用戶提供了一個社交登錄按鈕,該按鈕指向 OAuth 2 提供者控制的網頁,並請求我們的應用程序訪問用戶個人資料的某些方面的權限。
不過這次 URL 看起來有點不同:
https://oauth2provider.com/auth? response_type=token& client_id=CLIENT_KEY& redirect_uri=CALLBACK_URI& scope=profile& scope=email
請注意,這次 URL 中的response_type
參數是token
。
那麼重定向URI呢?
這只是前端準備適當處理訪問令牌的任何地址。
根據使用的 OAuth 2 庫,前端實際上可能會在用戶設備上臨時運行能夠接受 HTTP 請求的服務器; 在這種情況下,重定向 URL 的格式為http://localhost:7862/callback/?token=TOKEN
。
因為 OAuth 2 服務器在用戶接受後返回 HTTP 重定向,並且此重定向由用戶設備上的瀏覽器處理,所以此地址被正確解釋,從而使前端可以訪問令牌。
或者,前端可以直接實現合適的頁面。 無論哪種方式,此時前端都負責解析查詢參數和處理訪問令牌。
從此時起,前端可以使用令牌直接調用 OAuth 2 提供者的 API。 但是用戶並不真正想要那樣。 他們希望通過身份驗證訪問您的 API。 後端需要提供的只是一個端點,前端可以在該端點上將社交提供者的訪問令牌交換為授予對 API 訪問權限的令牌。
考慮到向前端提供訪問令牌本質上不如服務器端流程安全,為什麼要允許這樣做呢?
客戶端流程允許在後端 REST API 和麵向用戶的前端之間進行更嚴格的分離。 沒有什麼能嚴格阻止您將後端服務器指定為重定向 URI; 最終效果將是某種混合流。
問題是服務器必須生成一個適當的面向用戶的頁面,然後以某種方式將控制權交還給前端。
在現代項目中,嚴格區分前端 UI 和處理所有業務邏輯的後端之間的關注點是很常見的。 它們通常通過定義明確的 JSON API 進行通信。 然而,上面描述的混合流程混淆了關注點的分離,迫使後端既服務於面向用戶的頁面,然後設計一些流程以某種方式將控制權交還給前端。
允許前端處理訪問令牌是一種保留關注點分離的權宜之計。 它在一定程度上增加了來自受感染客戶端的風險,但總的來說效果很好。
這個流程對於前端來說可能看起來很複雜,如果你需要前端團隊自己開發所有東西,那就是複雜的。 但是,Facebook 和 Google 都提供了庫,這些庫使前端能夠包含登錄按鈕,從而以最少的配置處理整個過程。

這是後端令牌交換的秘訣。
在客戶端流程下,後端與 OAuth 2 流程完全隔離。 不要被誤導:這不是一項簡單的工作。 您會希望它至少支持以下功能。
- 至少向 OAuth 2 提供者發送一個請求,只是為了確保前端提供的令牌是有效的,而不是一些任意的隨機字符串。
- 當令牌有效時,為您的 API 返回一個有效令牌。 否則,返回信息錯誤。
- 如果這是一個新用戶,為他們創建一個
User
模型,並適當地填充它。 - 如果這是一個已經存在
User
模型的用戶,請通過他們的電子郵件地址匹配他們,這樣他們就可以訪問正確的現有帳戶,而不是為社交登錄創建一個新帳戶。 - 根據用戶在社交媒體上提供的內容更新用戶的個人資料詳細信息。
好消息是在後端實現所有這些功能比您想像的要簡單得多。
這就是如何在短短兩打代碼中讓所有這些在後端工作的魔力。 這取決於 Python Social Auth 庫(以下稱為“PSA”),因此您需要在requirements.txt
中包含social-auth-core
和social-auth-app-django
。
您還需要按照此處記錄的方式配置庫。 請注意,為清楚起見,這不包括一些異常處理。
可以在此處找到此示例的完整代碼。
@api_view(http_method_names=['POST']) @permission_classes([AllowAny]) @psa() def exchange_token(request, backend): serializer = SocialSerializer(data=request.data) if serializer.is_valid(raise_exception=True): # This is the key line of code: with the @psa() decorator above, # it engages the PSA machinery to perform whatever social authentication # steps are configured in your SOCIAL_AUTH_PIPELINE. At the end, it either # hands you a populated User model of whatever type you've configured in # your project, or None. user = request.backend.do_auth(serializer.validated_data['access_token']) if user: # if using some other token back-end than DRF's built-in TokenAuthentication, # you'll need to customize this to get an appropriate token object token, _ = Token.objects.get_or_create(user=user) return Response({'token': token.key}) else: return Response( {'errors': {'token': 'Invalid token'}}, status=status.HTTP_400_BAD_REQUEST, )
在您的設置(完整代碼)中只需要添加一點內容,然後您就完成了:
AUTHENTICATION_BACKENDS = ( 'social_core.backends.google.GoogleOAuth2', 'social_core.backends.facebook.FacebookOAuth2', 'django.contrib.auth.backends.ModelBackend', ) for key in ['GOOGLE_OAUTH2_KEY', 'GOOGLE_OAUTH2_SECRET', 'FACEBOOK_KEY', 'FACEBOOK_SECRET']: # Use exec instead of eval here because we're not just trying to evaluate a dynamic value here; # we're setting a module attribute whose name varies. exec("SOCIAL_AUTH_{key} = os.environ.get('{key}')".format(key=key)) SOCIAL_AUTH_PIPELINE = ( 'social_core.pipeline.social_auth.social_details', 'social_core.pipeline.social_auth.social_uid', 'social_core.pipeline.social_auth.auth_allowed', 'social_core.pipeline.social_auth.social_user', 'social_core.pipeline.user.get_username', 'social_core.pipeline.social_auth.associate_by_email', 'social_core.pipeline.user.create_user', 'social_core.pipeline.social_auth.associate_user', 'social_core.pipeline.social_auth.load_extra_data', 'social_core.pipeline.user.user_details', )
在urls.py
中添加到這個函數的映射,一切就緒!
那個魔法是怎麼起作用的?
Python Social Auth 是一個非常酷、非常複雜的機器。 它非常樂意處理數十個社交身份驗證提供程序中的任何一個的身份驗證和訪問,並且它適用於大多數流行的 Python Web 框架,包括 Django、Flask、Pyramid、CherryPy 和 WebPy。
在大多數情況下,上面的代碼是一個非常標準的基於 Django REST 框架 (DRF) 函數的視圖:它偵聽您在urls.py
中映射到的任何路徑上的 POST 請求,並假設您在它期望的格式,然後它會為您提供一個User
對像或None
。
如果你得到一個User
對象,它就是你在項目其他地方配置的模型類型,它可能已經存在也可能不存在。 PSA 已經負責驗證令牌、識別是否存在用戶匹配、在必要時創建用戶以及從社交提供者更新用戶詳細信息。
用戶如何從社交提供者的用戶映射到您的用戶以及如何與現有用戶關聯的確切細節由上面定義的SOCIAL_AUTH_PIPELINE
指定。 關於這一切的工作原理還有很多需要了解,但這超出了本文的範圍。 你可以在這裡讀更多關於它的內容。
魔術的關鍵部分是視圖上的@psa()
裝飾器,它將一些成員添加到傳遞到您的視圖的request
對像中。 對我們來說最有趣的是request.backend
(對於 PSA,後端是任何社交身份驗證提供程序)。
為我們選擇了適當的後端,並根據視圖的backend
參數附加到request
對象,該視圖由 URL 本身填充。
一旦您掌握了backend
對象,很高興根據您的訪問代碼對您進行身份驗證。 這就是do_auth
方法。 反過來,這會從您的配置文件中使用整個SOCIAL_AUTH_PIPELINE
。
如果你擴展它,管道可以做一些非常強大的事情,儘管它已經完成了你需要它做的所有事情,除了它的默認內置功能。
之後,就回到正常的 DRF 代碼:如果你有一個有效的User
對象,你可以很容易地返回一個適當的 API 令牌。 如果您沒有取回有效的User
對象,則很容易產生錯誤。
這種技術的一個缺點是,雖然在發生錯誤時返回錯誤相對簡單,但很難深入了解具體出了什麼問題。 PSA 吞下服務器可能返回的有關問題所在的任何細節。
再說一次,設計良好的身份驗證系統的本質是對錯誤源相當不透明。 如果應用程序在嘗試登錄後告訴用戶“密碼無效”,那無異於說“恭喜! 你猜到了一個有效的用戶名。”
為什麼不自己推出呢?
一句話:可擴展性。 很少有社交 OAuth 2 提供商以完全相同的方式在其 API 調用中要求或返回完全相同的信息。 雖然有各種特殊情況和例外。
設置 PSA 後添加新的社交提供者只需在設置文件中進行幾行配置即可。 您根本不需要調整任何代碼。 PSA 將所有這些抽像出來,以便您可以專注於自己的應用程序。
我到底該如何測試呢?
好問題! unittest.mock
不適合模擬隱藏在庫深處的抽象層下的 API 調用; 僅僅發現模擬的精確路徑將需要大量的努力。
相反,由於 PSA 構建在 Requests 庫之上,您可以使用出色的 Responses 庫在 HTTP 級別模擬提供程序。
對測試的完整討論超出了本文的範圍,但此處包含了我們的測試示例。 需要注意的特定功能是SocialAuthTests
mocked
。
讓 PSA 承擔重任。
OAuth2 過程詳細而復雜,具有很多固有的複雜性。 幸運的是,通過引入一個專門用於以盡可能輕鬆的方式處理它的庫,可以繞過大部分複雜性。
Python Social Auth 在這方面做得很好。 我們演示了一個 Django/DRF 視圖,它利用客戶端、隱式、OAuth2 流來實現無縫用戶創建和匹配,只需 25 行代碼。 這還不算太破舊。