简体   繁体   English

Django + Google联合登录

[英]Django + Google Federated Login

I would like to enable the visitors of my website to login using their Google Accounts instead of having to sign up and create a new one. 我想让我网站的访问者使用他们的Google帐户登录,而不必注册并创建一个新帐户。

A few things: 一些东西:

  • I am NOT using the Django authentication framework, instead, I do my own authentication and keep information about users in my own set of tables 我没有使用Django身份验证框架,而是我自己进行身份验证并在我自己的表集中保留有关用户的信息
  • consequently, various django-openid libs are not applicable since they all assume that the standard Django auth framework is used. 因此,各种django-openid库不适用,因为它们都假设使用了标准的Django auth框架。

I tried to study the python-openid library + the google federated login API but I am lost. 我试图研究python-openid库+ google联合登录API,但我迷路了。 I get as close as to understand the instantiating the Consumer class but do not understand the session and store params required. 我尽可能地了解实例化Consumer类但不了解会话并存储所需的params。 I cannot fathom something that seems so easy can be so complicated. 我无法理解看起来如此简单的事情会如此复杂。 Is there really no step by step tutorial of how to do this in pure python or django? 是否真的没有一步一步的教程如何在纯python或django中做到这一点?

I tried to look at the examples/consumer.py within the python-openid but it's 500lines of code again that I do not understand. 我试着看一下python-openid中的examples / consumer.py,但是我不理解它的500行代码。

I also don't understand how verification of the user against google accounts is done on every request to my website. 我也不明白如何在每次向我的网站发出请求时对用户进行Google帐户验证。 Google API only explains initial login steps. Google API仅说明了初始登录步骤。 What happens on every request to my website where authentication must be verified against a google server? 每次向我的网站发出必须针对谷歌服务器验证身份验证的请求会发生什么?

I think your problem stems from a basic misunderstanding of how OpenID and/or OAuth work. 我认为您的问题源于对OpenID和/或OAuth如何工作的基本误解。

It looks like you just want authentication, so let's stick to OpenID for now. 看起来你只想要身份验证,所以现在让我们坚持使用OpenID。 You are correct to look at existing libraries. 查看现有库是正确的。 python-openid is the one to use if you only need OpenID and not OAuth, and you are not using Django's built-in auth framework. 如果你只需要OpenID而不是OAuth,那么你可以使用python-openid,而你没有使用Django的内置auth框架。

The full documentation for Federated Login with OpenID and OAuth is here: http://code.google.com/apis/accounts/docs/OpenID.html . 有关OpenID和OAuth的联合登录的完整文档位于: http//code.google.com/apis/accounts/docs/OpenID.html In particular, look at the diagram under "Interaction sequence". 特别是,请查看“交互序列”下的图表。

First, here is a very good working example from Facebook's Tornado web server's auth module: 首先,这是来自Facebook的Tornado Web服务器的auth模块的一个非常好的工作示例:

https://github.com/facebook/tornado/blob/master/tornado/auth.py (grep that for "GoogleHandler". I've used it with great success.) This is independent of Django and Django auth, and should give you a good example of how to implement what you want. https://github.com/facebook/tornado/blob/master/tornado/auth.py(grep表示“GoogleHandler”。我使用它非常成功。)这是独立于Django和Django auth,并且应该给你一个如何实现你想要的好例子。 If that's still not enough, read on... 如果仍然不够,请继续阅读......

You said django-openid is irrelevant, but in fact it demonstrates an implementation of exactly what you want, but for Django's auth system instead of yours. 你说django-openid是无关紧要的,但实际上它展示了你想要的实现,但是对于Django的auth系统而不是你的。 Actually, you should look at the similar plugin, Django-SocialAuth , which implements OpenID + OAuth for a few different providers (Google, Facebook, Twitter, etc.). 实际上,你应该看一下类似的插件Django-SocialAuth ,它为几个不同的提供商(Google,Facebook,Twitter等)实现了OpenID + OAuth。 In particular, look at: 特别要看:

https://github.com/agiliq/Django-Socialauth/blob/master/socialauth/lib/oauthgoogle.py and https://github.com/agiliq/Django-Socialauth/tree/master/openid_consumer and https://github.com/agiliq/Django-Socialauth/tree/master/example_project https://github.com/agiliq/Django-Socialauth/blob/master/socialauth/lib/oauthgoogle.pyhttps://github.com/agiliq/Django-Socialauth/tree/master/openid_consumerhttps:// github.com/agiliq/Django-Socialauth/tree/master/example_project

...for a full working example using django's auth framework, and can be adapted to your custom auth framework. ...使用django的auth框架的完整工作示例,并可以适应您的自定义身份验证框架。

Best of luck. 祝你好运。 I encourage you to document whatever ends up working for you and build a step-by-step guide for others like you. 我鼓励您记录最终为您工作的内容,并为像您这样的其他人建立分步指南。

I have managed to demistify the problem so here is the solution and I hope someone else can benefit from it: 1) Google Account verification is not done against the google accounts server on every request to your application. 我设法解决了问题,所以这里是解决方案,我希望其他人可以从中受益:1)Google应用程序验证不是针对您的应用程序的每个请求对谷歌帐户服务器进行的。 For example: 1.1 a user logs into your app using their gmail account 1.2 the user also navigates to gmail.com where they check their email 1.3 they log out of gmail 1.4 they remain logged into your application and can use it fully This means you have to take care of session expiry on your end, Google account does not take care of it. 例如:1.1用户使用他们的Gmail帐户登录您的应用程序1.2用户也导航到gmail.com,他们检查他们的电子邮件1.3他们退出gmail 1.4他们仍然登录到您的应用程序并可以完全使用它这意味着你有为了照顾你的会话到期,谷歌帐户不会照顾它。

2) The core Python code I used is the following: 2)我使用的核心Python代码如下:

from openid.consumer.consumer import Consumer, \
    SUCCESS, CANCEL, FAILURE, SETUP_NEEDED
from openid.consumer.discover import DiscoveryFailure
from django.utils.encoding import smart_unicode
from myapp.common.util.openid import DjangoOpenIDStore

def google_signin(request):
    """ This is the view where the Google account login icon on your site points to, e.g. http://www.yourdomain.com/google-signin """
    consumer = Consumer(request.session, DjangoOpenIDStore())

    # catch Google Apps domain that is referring, if any 
    _domain = None
    if 'domain' in request.POST:
        _domain = request.POST['domain']
    elif 'domain' in request.GET:
        _domain = request.GET['domain']

    try:
        # two different endpoints depending on whether the using is using Google Account or Google Apps Account
        if _domain:
            auth_request = consumer.begin('https://www.google.com/accounts/o8/site-xrds?hd=%s' % _domain)
        else:
            auth_request = consumer.begin('https://www.google.com/accounts/o8/id')
    except DiscoveryFailure as e:
        return CustomError(request, "Google Accounts Error", "Google's OpenID endpoint is not available.")

    # add requests for additional account information required, in my case: email, first name & last name
    auth_request.addExtensionArg('http://openid.net/srv/ax/1.0', 'mode', 'fetch_request')
    auth_request.addExtensionArg('http://openid.net/srv/ax/1.0', 'required', 'email,firstname,lastname')
    auth_request.addExtensionArg('http://openid.net/srv/ax/1.0', 'type.email', 'http://schema.openid.net/contact/email')
    auth_request.addExtensionArg('http://openid.net/srv/ax/1.0', 'type.firstname', 'http://axschema.org/namePerson/first')
    auth_request.addExtensionArg('http://openid.net/srv/ax/1.0', 'type.lastname', 'http://axschema.org/namePerson/last')

    return redirect(auth_request.redirectURL('http://www.yourdomain.com', 'http://www.yourdomain.com/google-signin-response')))


@transaction.commit_manually 
def google_signin_response(request):
    """ Callback from Google Account service with login the status. Your url could be http://www.yourdomain.com/google-signin-response """
    transaction.rollback() # required due to Django's transaction inconsistency between calls
    oidconsumer = Consumer(request.session, DjangoOpenIDStore())

    # parse GET parameters submit them with the full url to consumer.complete
    _params = dict((k,smart_unicode(v)) for k, v in request.GET.items())
    info = oidconsumer.complete(_params, request.build_absolute_uri().split('?')[0])
    display_identifier = info.getDisplayIdentifier()

    if info.status == FAILURE and display_identifier:
        return CustomError(request, _("Google Login Error"), _("Verification of %(user)s failed: %(error_message)s") % {'user' : display_identifier, 'error_message' : info.message})

    elif info.status == SUCCESS:
        try:
            _email = info.message.args[('http://openid.net/srv/ax/1.0', 'value.email')]
            _first_name = info.message.args[('http://openid.net/srv/ax/1.0', 'value.firstname')]
            _last_name = info.message.args[('http://openid.net/srv/ax/1.0', 'value.lastname')]
            try:
                _user = User.objects.get(email__iexact=_email)
            except ObjectDoesNotExist:
                # create a new account if one does not exist with the authorized email yet and log that user in
                _new_user = _new_account(_email, _first_name + ' ' + _last_name, _first_name, _last_name, p_account_status=1)
                _login(request, _new_user, info.message.args[('http://specs.openid.net/auth/2.0', 'response_nonce')])
                transaction.commit()
                return redirect('home')
            else:
                # login existing user
                _login(request, _user, info.message.args[('http://specs.openid.net/auth/2.0', 'response_nonce')])
                transaction.commit()
                return redirect('home')
        except Exception as e:
            transaction.rollback()
            system_log_entry(e, request=request)
            return CustomError(request, _("Login Unsuccessful"), "%s" % e)

    elif info.status == CANCEL:
        return CustomError(request, _("Google Login Error"), _('Google account verification cancelled.'))

    elif info.status == SETUP_NEEDED:
        if info.setup_url:
            return CustomError(request, _("Google Login Setup Needed"), _('<a href="%(url)s">Setup needed</a>') % { 'url' : info.setup_url })
        else:
            # This means auth didn't succeed, but you're welcome to try
            # non-immediate mode.
            return CustomError(request, _("Google Login Setup Needed"), _('Setup needed'))
    else:
        # Either we don't understand the code or there is no
        # openid_url included with the error. Give a generic
        # failure message. The library should supply debug
        # information in a log.
        return CustomError(request, _("Google Login Error"), _('Google account verification failed for an unknown reason. Please try to create a manual account on Acquee.'))


def get_url_host(request):
    if request.is_secure():
        protocol = 'https'
    else:
        protocol = 'http'
    host = escape(get_host(request))
    return '%s://%s' % (protocol, host)

3) an additional lib I created and imported above (myapp.common.util.openid) is a merge of a few existing Django openID libs so kudos to those guys: 3)我在上面创建和导入的另一个库(myapp.common.util.openid)是一些现有的Django openID库的合并,所以对这些家伙赞不绝口:

from django.db import models
from django.conf import settings
from django.utils.hashcompat import md5_constructor

from openid.store.interface import OpenIDStore
import openid.store 
from openid.association import Association as OIDAssociation
import time, base64

from myapp.common.db.accounts.models import Association, Nonce

class DjangoOpenIDStore(OpenIDStore):
    """
The Python openid library needs an OpenIDStore subclass to persist data
related to OpenID authentications. This one uses our Django models.
"""

    def storeAssociation(self, server_url, association):
        assoc = Association(
            server_url = server_url,
            handle = association.handle,
            secret = base64.encodestring(association.secret),
            issued = association.issued,
            lifetime = association.issued,
            assoc_type = association.assoc_type
        )
        assoc.save()

    def getAssociation(self, server_url, handle=None):
        assocs = []
        if handle is not None:
            assocs = Association.objects.filter(
                server_url = server_url, handle = handle
            )
        else:
            assocs = Association.objects.filter(
                server_url = server_url
            )
        if not assocs:
            return None
        associations = []
        for assoc in assocs:
            association = OIDAssociation(
                assoc.handle, base64.decodestring(assoc.secret), assoc.issued,
                assoc.lifetime, assoc.assoc_type
            )
            if association.getExpiresIn() == 0:
                self.removeAssociation(server_url, assoc.handle)
            else:
                associations.append((association.issued, association))
        if not associations:
            return None
        return associations[-1][1]

    def removeAssociation(self, server_url, handle):
        assocs = list(Association.objects.filter(
            server_url = server_url, handle = handle
        ))
        assocs_exist = len(assocs) > 0
        for assoc in assocs:
            assoc.delete()
        return assocs_exist

    def useNonce(self, server_url, timestamp, salt):
        # Has nonce expired?
        if abs(timestamp - time.time()) > openid.store.nonce.SKEW:
            return False
        try:
            nonce = Nonce.objects.get(
                server_url__exact = server_url,
                timestamp__exact = timestamp,
                salt__exact = salt
            )
        except Nonce.DoesNotExist:
            nonce = Nonce.objects.create(
                server_url = server_url,
                timestamp = timestamp,
                salt = salt
            )
            return True
        nonce.delete()
        return False

    def cleanupNonce(self):
        Nonce.objects.filter(
            timestamp__lt = (int(time.time()) - nonce.SKEW)
        ).delete()

    def cleaupAssociations(self):
        Association.objects.extra(
            where=['issued + lifetimeint < (%s)' % time.time()]
        ).delete()

    def getAuthKey(self):
        # Use first AUTH_KEY_LEN characters of md5 hash of SECRET_KEY
        return md5_constructor.new(settings.SECRET_KEY).hexdigest()[:self.AUTH_KEY_LEN]

    def isDumb(self):
        return False

4) and the model that is required in order to hold google account session identifiers and verified endpoints: 4)以及保存Google帐户会话标识符和已验证端点所需的模型:

class Nonce(models.Model):
    """ Required for OpenID functionality """
    server_url = models.CharField(max_length=255)
    timestamp = models.IntegerField()
    salt = models.CharField(max_length=40)

    def __unicode__(self):
        return u"Nonce: %s for %s" % (self.salt, self.server_url)


class Association(models.Model):
    """ Required for OpenID functionality """
    server_url = models.TextField(max_length=2047)
    handle = models.CharField(max_length=255)
    secret = models.TextField(max_length=255) # Stored base64 encoded
    issued = models.IntegerField()
    lifetime = models.IntegerField()
    assoc_type = models.TextField(max_length=64)

    def __unicode__(self):
        return u"Association: %s, %s" % (self.server_url, self.handle)

Good luck! 祝好运! Rok 韩国

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM