Django allauth社交登录:使用注册的电子邮件自动链接社交网站资料

49

我希望为我的Django网站的用户创建尽可能简便的登录体验。我想要实现以下步骤:

  1. 向用户呈现登录界面
  2. 用户选择使用Facebook或Google登录
  3. 用户在外部网站中输入密码
  4. 用户可以作为已认证的用户与我的网站交互

好的,这部分很容易,只需安装django-allauth并进行配置。

但我还想提供使用本地用户登录的选项。这将有另一个步骤:

  1. 向用户呈现登录界面
  2. 用户选择注册
  3. 用户输入凭据
  4. 网站发送验证电子邮件
  5. 用户点击电子邮件链接后,可以作为已认证的用户与我的网站进行交互

好的,默认身份验证和allauth都可以实现。但现在是百万美元的问题。

如果他们更改了登录方式,我如何自动关联他们的Google、FB和本地帐户?

请注意,无论他们以何种方式登录,我都有他们的电子邮件地址。使用django-allauth是否可以实现?我知道可以通过用户干预来实现。今天的默认行为是拒绝登录,说电子邮件已经注册。

如果仅仅通过配置无法实现,我将接受给我一些方向的答案,以指导我应该对allauth代码进行哪些修改以支持此工作流程。

有很多原因需要这样做。用户会忘记他们用哪种方法进行身份验证,有时会使用 Google,有时使用 FB,有时使用本地用户帐户。我们已经拥有了许多本地用户帐户,社交帐户将是一个新功能。我希望用户能够保持他们的身份。我预想可以请求用户的好友列表,所以如果他们使用 Google 登录,我也想要他们的 FB 帐户。

这是一个爱好网站,没有很高的安全要求,请不要回答说这不是明智的安全实现。

以后,我会创建自定义用户模型,只使用电子邮件作为登录ID。但如果能让我自动关联具有所需用户名的默认用户模型的帐户,我会很高兴得到答案。

我正在使用 Django==1.5.4 和 django-allauth==0.13.0。


django-social-auth正好符合你的需求。你可以将授权逻辑放在设置中的社交认证管道的任何位置。我不了解all-auth,但social-auth安装并开始使用并不难,它具有许多后端和非常灵活的功能。 - PukeCloud
3
@PukeCloud说:我没能成功使用django-social-auth。他们正在将项目转移到另一个名为python-social-auth的包中,而且文档真的很混乱。我几乎可以直接使用allauth。在网上阅读时,其他人也表示allauth更容易使用。看起来allauth正在获得动力。我会再等一段时间看看是否有人在这里回答。 - neves
Allauth支持将多个社交账户链接到一个用户,因此您可以仅使用allauth完成上述所有操作。 - elssar
啊,那就是你想要的。嗯,我不确定那是否可能,或者甚至是否应该这样做。 - elssar
1
啊,现在我明白了。如果一个已经注册的用户尝试使用与其注册时相同的电子邮件地址进行其他社交登录,您希望将其登录到已存在的帐户中。好的,我相信这是可能的。我会查看allauth的代码并回复您。 - elssar
显示剩余4条评论
5个回答

46
注意(2018年10月23日):我不再使用这种方法,因为其中包含太多的魔法。相反,我启用了SOCIALACCOUNT_EMAIL_REQUIRED'facebook': {'VERIFIED_EMAIL': False, ...}。这样,所有社交登录都会被重定向到一个社交注册表单中,以输入有效的电子邮件地址。如果已经注册,则会出现一个错误提示,要求先登录,然后连接账户。对我来说,目前已经足够公平了。
我正在尝试改进这种用例,并想出了以下解决方案:
from allauth.account.models import EmailAddress
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter

class SocialAccountAdapter(DefaultSocialAccountAdapter):
    def pre_social_login(self, request, sociallogin):
        """
        Invoked just after a user successfully authenticates via a
        social provider, but before the login is actually processed
        (and before the pre_social_login signal is emitted).

        We're trying to solve different use cases:
        - social account already exists, just go on
        - social account has no email or email is unknown, just go on
        - social account's email exists, link social account to existing user
        """

        # Ignore existing social accounts, just do this stuff for new ones
        if sociallogin.is_existing:
            return

        # some social logins don't have an email address, e.g. facebook accounts
        # with mobile numbers only, but allauth takes care of this case so just
        # ignore it
        if 'email' not in sociallogin.account.extra_data:
            return

        # check if given email address already exists.
        # Note: __iexact is used to ignore cases
        try:
            email = sociallogin.account.extra_data['email'].lower()
            email_address = EmailAddress.objects.get(email__iexact=email)

        # if it does not, let allauth take care of this new social account
        except EmailAddress.DoesNotExist:
            return

        # if it does, connect this new social login to the existing user
        user = email_address.user
        sociallogin.connect(request, user)

据我所测试,它似乎运行良好。但是我们非常欢迎您的输入和建议!

5
这种方法比被接受的答案要好得多。我正在使用它,并可能会加入一些修改;如果我想到任何好的改进,我会留下另一个评论。 - nhinkle
这确实比预期的答案更好,但我认为您假设了电子邮件地址已经通过验证,这样做是否可以? - aliasav
实际上不是这样的,请参考Victor Grau Serrat的回答。例如,Facebook并不清楚每个电子邮件地址是否经过验证。 - sspross
我基于上述解决方案,但对我有效的是,在除了EmailAddress.DoesNotExist之后加上return:我创建了缺失的电子邮件地址,然后它就正常工作了。 - PhoebeB
谢谢大家!它运行良好!我使用了Google API进行测试。 - Lorenzo Simonassi

31

您需要覆盖sociallogin适配器,具体来说是pre_social_login方法,在使用社交提供商进行身份验证后但在allauth处理此登录之前调用。

my_adapter.py中,可以这样做:

from django.contrib.auth.models import User

from allauth.account.models import EmailAccount
from allauth.exceptions import ImmediateHttpResponse
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter


class MyAdapter(DefaultSocialAccountAdapter):
    def pre_social_login(self, request, sociallogin):
        # This isn't tested, but should work
        try:
            user = User.objects.get(email=sociallogin.email)
            sociallogin.connect(request, user)
            # Create a response object
            raise ImmediateHttpResponse(response)
        except User.DoesNotExist:
            pass

在您的设置中,将社交适配器更改为您的适配器。

SOCIALACCOUNT_ADAPTER = 'myapp.my_adapter.MyAdapter`

您可以通过这种方式将多个社交账户连接到一个用户上。

1
运作得非常好!谢谢!但是获取用户的正确方法是: user = User.objects.get(email=sociallogin.account.user.email) - user1931780
1
实际上无法让它正常工作。除了import EmailAccount处的一些导入错误和上面的注释外,它还在assert not self.is_existing这一行失败。https://github.com/pennersr/django-allauth/blob/master/allauth/socialaccount/models.py#L219顺便说一下,账户正在被连接。能否提供完整的可工作示例?谢谢。 - Valentin Kantor
2
如果尝试连接已保存的社交账户,则会引发assert错误。因此,在连接之前添加if条件检查,以查看socialaccount是否有主键(pk)。如果有,则返回。 - Ahmed Aeon Axan
3
raise ImmediateHttpResponse(response) 语句中的 response 应该从哪里来? - nhinkle
@elssar,从下面的其他解决方案中看来,似乎只使用return比尝试使用raise ImmediateHttpResponse更有效。 - nhinkle
显示剩余2条评论

14
根据babus相关主题下的评论,之前发布的答案(1, 2)存在一个大的安全漏洞,所有认证文档中都有所记录:

"Facebook文档没有明确说明账户是否验证了电子邮件地址。例如,验证也可以通过电话或信用卡进行。为了保险起见,默认情况下将来自Facebook的电子邮件地址视为未经验证。"

换句话说,我可以使用您的电子邮件ID在Facebook上注册,或者在Facebook上更改我的电子邮件为您的电子邮件并登录网站以访问您的帐户。

因此,在考虑这一点并基于@sspross的答案的基础上,我的方法是将用户重定向到登录页面,并通知他们有重复的帐户,并邀请他们使用其他帐户登录,并在登录后链接它们。我承认这与原始问题不同,但这样做不会引入任何安全漏洞。

因此,我的适配器看起来像:

from django.contrib.auth.models import User
from allauth.account.models import EmailAddress
from allauth.exceptions import ImmediateHttpResponse
from django.shortcuts import redirect
from django.contrib import messages
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter

class MyAdapter(DefaultSocialAccountAdapter):
    def pre_social_login(self, request, sociallogin):
        """
        Invoked just after a user successfully authenticates via a
        social provider, but before the login is actually processed
        (and before the pre_social_login signal is emitted).

        We're trying to solve different use cases:
        - social account already exists, just go on
        - social account has no email or email is unknown, just go on
        - social account's email exists, link social account to existing user
        """

        # Ignore existing social accounts, just do this stuff for new ones
        if sociallogin.is_existing:
            return

        # some social logins don't have an email address, e.g. facebook accounts
        # with mobile numbers only, but allauth takes care of this case so just
        # ignore it
        if 'email' not in sociallogin.account.extra_data:
            return

        # check if given email address already exists.
        # Note: __iexact is used to ignore cases
        try:
            email = sociallogin.account.extra_data['email'].lower()
            email_address = EmailAddress.objects.get(email__iexact=email)

        # if it does not, let allauth take care of this new social account
        except EmailAddress.DoesNotExist:
            return

        # if it does, bounce back to the login page
        account = User.objects.get(email=email).socialaccount_set.first()
        messages.error(request, "A "+account.provider.capitalize()+" account already exists associated to "+email_address.email+". Log in with that instead, and connect your "+sociallogin.account.provider.capitalize()+" account through your profile page to link them together.")       
        raise ImmediateHttpResponse(redirect('/accounts/login'))

4
有没有办法在Facebook上检查电子邮件是否已确认? - Aditya Pandhare
3
如果您真的想要“将社交账户链接到现有用户”,则必须使用sociallogin.connect(request, email_address.user)替换“如果是,则返回登录页面”的部分。 - imbolc
2
我已经阅读过,如果FB返回电子邮件,则保证为经过验证的电子邮件,因此实际上不再存在安全问题。 - Evan Zamir
1
@EvanZamir 是的,我也读到了,FB现在保证只返回已验证的电子邮件。 - timbram

4

我刚在源代码中发现了这个评论:

        if account_settings.UNIQUE_EMAIL:
            if email_address_exists(email):
                # Oops, another user already has this address.  We
                # cannot simply connect this social account to the
                # existing user. Reason is that the email adress may
                # not be verified, meaning, the user may be a hacker
                # that has added your email address to his account in
                # the hope that you fall in his trap.  We cannot check
                # on 'email_address.verified' either, because
                # 'email_address' is not guaranteed to be verified.

因此,这是设计上不可能实现的。


有没有任何钩子或技巧可以绕过这个问题?我在这里卡住了 :/ - Anmol

1
如果他们更改了登录方式,我如何自动关联他们的Google、FB和本地账户?
这是可能的,但您必须注意安全问题。请检查以下场景:
1. 用户通过电子邮件和密码在您的网站上创建帐户。用户没有Facebook帐户。 2. 攻击者使用用户的电子邮件在Facebook上创建帐户。(虽然这是一种假设的情况,但您无法控制社交网络是否验证电子邮件。) 3. 攻击者使用Facebook登录到您的网站,并自动访问用户原始帐户。
但您可以解决此问题。我描述了解决方法,请参考https://github.com/pennersr/django-allauth/issues/1149 愉快的场景应该是:
  1. 用户通过输入电子邮件和密码在您的网站上创建帐户。用户注销。
  2. 用户忘记了他的帐户,试图通过Facebook登录。
  3. 系统通过Facebook对用户进行身份验证,并发现他已经通过其他方法创建了帐户(电子邮件相同)。系统将用户重定向到普通登录页面,并显示消息“您已经使用电子邮件和密码创建了帐户。请以这种方式登录。登录后,您将能够使用Facebook登录。”
  4. 用户通过电子邮件和密码登录。
  5. 系统自动将他的Facebook登录与他的帐户连接起来。下次用户可以使用Facebook登录或电子邮件和密码。

但是Facebook会要求电子邮件验证,不是吗? - Lorenzo Simonassi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接