Django + Google联合登录

Rok*_*Rok 4 python openid django single-sign-on

我想让我网站的访问者使用他们的Google帐户登录,而不必注册并创建一个新帐户.

一些东西:

  • 我没有使用Django身份验证框架,而是我自己进行身份验证并在我自己的表集中保留有关用户的信息
  • 因此,各种django-openid库不适用,因为它们都假设使用了标准的Django auth框架.

我试图研究python-openid库+ google联合登录API,但我迷路了.我尽可能地了解实例化Consumer类但不了解会话并存储所需的params.我无法理解看起来如此简单的事情会如此复杂.是否真的没有一步一步的教程如何在纯python或django中做到这一点?

我试着看一下python-openid中的examples/consumer.py,但是我不理解它的500行代码.

我也不明白如何在每次向我的网站发出请求时对用户进行Google帐户验证.Google API仅说明了初始登录步骤.每次向我的网站发出必须针对谷歌服务器验证身份验证的请求会发生什么?

小智 10

我认为您的问题源于对OpenID和/或OAuth如何工作的基本误解.

看起来你只想要身份验证,所以现在让我们坚持使用OpenID.查看现有库是正确的.如果你只需要OpenID而不是OAuth,那么你可以使用python-openid,而你没有使用Django的内置auth框架.

有关OpenID和OAuth的联合登录的完整文档位于:http: //code.google.com/apis/accounts/docs/OpenID.html.特别是,请查看"交互序列"下的图表.

首先,这是来自Facebook的Tornado Web服务器的auth模块的一个非常好的工作示例:

https://github.com/facebook/tornado/blob/master/tornado/auth.py(grep 表示"GoogleHandler".我使用它非常成功.)这是独立于Django和Django auth,并且应该给你一个如何实现你想要的好例子.如果仍然不够,请继续阅读......

你说django-openid是无关紧要的,但实际上它展示了你想要的实现,但是对于Django的auth系统而不是你的.实际上,你应该看一下类似的插件Django-SocialAuth,它为几个不同的提供商(Google,Facebook,Twitter等)实现了OpenID + OAuth.特别要看:

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

...使用django的auth框架的完整工作示例,并可以适应您的自定义身份验证框架.

祝你好运.我鼓励您记录最终为您工作的内容,并为像您这样的其他人建立分步指南.


Rok*_*Rok 3

我已经成功地消除了这个问题,所以这里是解决方案,我希望其他人可以从中受益:1)Google帐户验证并不是针对您的应用程序的每个请求都针对Google帐户服务器进行的。例如: 1.1 用户使用其 gmail 帐户登录您的应用程序 1.2 用户还导航到 gmail.com,在那里检查电子邮件 1.3 他们注销 gmail 1.4 他们仍然登录到您的应用程序并可以充分使用它 这意味着您拥有为了解决您的会话过期问题,Google 帐户不会处理它。

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)
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

祝你好运!洛克