个性化阅读
专注于IT技术分析

将OAuth 2集成到你的Django/DRF后端

本文概述

我们都去过那里。你正在开发API后端, 并对它的运行状况感到满意。你最近完成了最低限度可行的产品(MVP), 所有测试都通过了, 并且你希望实现一些新功能。

然后老板给你发送电子邮件:”顺便说一句, 我们需要让人们通过Facebook和Google登录;他们不必只为像我们这样的小型网站创建帐户。”

大。范围蠕动再次出现。

好消息是, OAuth 2已经成为社交和第三方身份验证的行业标准(由Facebook, Google等服务使用), 因此你可以专注于理解和实施该标准以支持广泛的社交身份验证提供程序。

你可能对OAuth 2不熟悉;当这件事发生在我身上时, 我不是。

将OAuth 2集成到你的Django/DRF后端

作为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, 你的应用程序将可以在该URL上接收请求。作为交换, 他们收到客户密钥和客户机密。这些令牌在身份验证过程中进行交换, 以验证登录请求。

令牌将你的服务器代码称为客户端。主机是OAuth 2提供程序。它们并不适合你API的客户。

如何在不疯狂的情况下将OAuth 2集成到你的Django/DRF后端2

当你的应用程序生成包含按钮的页面时, 流程开始, 例如”使用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集成到你的Django/DRF后端3

假设用户批准, 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参数是令牌。

那么重定向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进行通信。尽管上述混合流程使关注点分离变得混乱, 但它迫使后端都为面向用户的页面提供服务, 然后设计了一些流程以某种方式将控制权交还给前端。

允许前端处理访问令牌是一种权宜之计, 可保留关注点分离。从某种程度上来说, 这增加了来自受感染客户的风险, 但总体而言, 它运作良好。

如何在不疯狂的情况下将OAuth 2集成到你的Django/DRF后端4

对于前端来说, 这种流程可能看起来很复杂, 也就是说, 如果你需要前端团队自行开发所有内容, 那么这是很困难的。但是, Facebook和Google都提供了使前端能够包括登录按钮的库, 这些按钮以最小的配置即可处理整个过程。

这是后端进行令牌交换的方法。

在客户端流下, 后端与OAuth 2流程完全隔离。不要被误导:这不是简单的工作。你将希望它至少支持以下功能。

  • 向OAuth 2提供程序发送至少一个请求, 只是为了确保前端提供的令牌有效, 而不是一些任意的随机字符串。
  • 令牌有效时, 请为你的API返回有效令牌。否则, 返回提示错误。
  • 如果这是新用户, 则为他们创建一个用户模型, 并适当地填充它。
  • 如果该用户已经存在用户模型, 则通过其电子邮件地址匹配他们, 这样他们就可以访问正确的现有帐户, 而无需为社交登录创建新的帐户。
  • 根据用户在社交媒体上提供的信息更新用户的个人资料详细信息。

好消息是, 在后端实现所有这些功能比你预期的要简单得多。

这是仅用两行代码即可在后端实现所有这些功能的魔力。这取决于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网络框架上使用, 包括Django, Flask, Pyramid, CherryPy和WebPy。

在大多数情况下, 上面的代码是一个非常标准的基于Django REST框架(DRF)的函数视图:它在urls.py中将其映射到的任何路径上侦听POST请求, 并假定你在预期的格式, 然后为你提供一个User对象, 即None。

如果你获得一个用户对象, 则该对象属于你在项目中其他位置配置的模型类型, 该模型类型可能已经存在或可能不存在。 PSA已经负责验证令牌, 识别是否存在用户匹配项, 在必要时创建用户以及从社交提供程序更新用户详细信息。

上面定义的SOCIAL_AUTH_PIPELINE指定了如何将用户从社交提供者的用户映射到你的用户并与现有用户相关联的确切详细信息。关于这一切的工作原理还有很多要了解的内容, 但这超出了本文的范围。你可以在这里读更多关于它的内容。

魔术的关键是视图上的@psa()装饰器, 该装饰器将一些成员添加到传递给你的视图的请求对象中。对我们而言, 最有趣的一个是request.backend(对于PSA, 后端是任何社交身份验证提供程序)。

为我们选择了适当的后端, 并根据视图的backend参数将其附加到请求对象, 该参数由URL本身填充。

一旦拥有了后端对象, 就很高兴根据给定的访问代码针对该提供者进行身份验证;那就是do_auth方法。反过来, 这会占用你配置文件中的全部SOCIAL_AUTH_PIPELINE。

如果你扩展了该管道, 它可以做一些非常强大的事情, 尽管它已经完成了你需要的一切, 而仅是其默认的内置功能。

之后, 它又回到了普通的DRF代码:如果你获得了有效的User对象, 则可以非常轻松地返回适当的API令牌。如果你没有找回有效的User对象, 则很容易产生错误。

这种技术的一个缺点是, 虽然返回错误(如果发生)相对简单, 但很难深入了解具体出了什么问题。 PSA吞下服务器可能返回的有关问题所在的任何详细信息。

再说一次, 对于错误源而言, 经过精心设计的身份验证系统本质上是完全不透明的。如果应用程序在尝试登录后告诉用户”无效密码”, 就等于说”恭喜!你猜到了有效的用户名。”

为什么不自己动手呢?

一句话:可扩展性。很少有社交OAuth 2提供程序在API调用中以完全相同的方式要求或返回完全相同的信息。尽管有各种特殊情况和例外。

在设置好PSA之后添加新的社交服务提供商, 只需在设置文件中进行几行配置即可。你根本不需要调整任何代码。 PSA将所有内容抽象出来, 因此你可以专注于自己的应用程序。

我该如何测试呢?

好问题! unittest.mock不适合模拟隐藏在库内部抽象层下的API调用;仅发现模拟的精确路径将需要大量的精力。

相反, 由于PSA是在Requests库之上构建的, 因此你可以使用出色的Responses库在HTTP级别模拟提供程序。

关于测试的完整讨论超出了本文的范围, 但是此处包含了我们的测试示例。需要注意的特殊功能是模拟的上下文管理器和SocialAuthTests类。

让PSA承担繁重的工作。

OAuth2流程非常详细且复杂, 具有许多固有的复杂性。幸运的是, 可以引入一个专用于尽可能简单地处理它的库来绕开很多复杂性。

Python Social Auth在这方面做得很好。我们已经展示了Django / DRF视图, 该视图利用客户端的隐式OAuth2流在25行代码中无缝地创建和匹配用户。不太破旧。

赞(0)
未经允许不得转载:srcmini » 将OAuth 2集成到你的Django/DRF后端

评论 抢沙发

评论前必须登录!