如何在不发疯的情况下将 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 行代码。 这还不算太破旧。