Django项目加入OAuth2.0授权登录

OAuth2.0

获取应用Id和秘钥

开发之前,需要前往第三方登录的开发者平台QQ新浪微博Github,注册账号并填写信息申请接入,成功后会给你一个ID和秘钥,以后你就通过该ID和秘钥来获取令牌,从而实现第三方登录。申请ID和秘钥时Github不需要审核,很简单,而QQ和新浪微博需要审核,稍微麻烦一点。

settings.py

#Oauth

#github
GITHUB_APP_ID = '************'

GITHUB_KEY = '************'

GITHUB_CALLBACK_URL = 'http://127.0.0.1:8000/oauth/github_check'  #填写你的回调地址

#QQ
QQ_APP_ID = '************'

QQ_KEY = '*******************8'

QQ_CALLBACK_URL = 'http://127.0.0.1:8000/oauth/qq_check'    #填写你的回调地址

#新浪微博
WEIBO_APP_ID = '************'

WEIBO_KEY = '*****************'

WEIBO_CALLBACK_URL = 'http://127.0.0.1:8000/oauth/weibo_check'    #填写你的回调地址

models.py

from django.db import models
from users.models import User

#用户登录的类型
type=(
    ('1','github'),
    ('2','qq'),
    ('3','weibo')
)

class OAuth_ex(models.Model):
    user = models.ForeignKey(User)           #User为本网站的用户模型,每个第三方账号都要绑定本站账号
    openid = models.CharField(max_length=100,default='')
    type = models.CharField(max_length=1,choices=type)

urls.py

from django.conf.urls import url
from . import views

app_name='oauth'

urlpatterns=[
    url(r'github_login',views.git_login,name='github_login'),
    url(r'github_check',views.git_check,name='github_check'),
    url(r'qq_login',views.qq_login,name='qq_login'),
    url(r'qq_check',views.qq_check,name='qq_check'),
    url(r'weibo_login',views.weibo_login,name='weibo_login'),
    url(r'weibo_check',views.weibo_check,name='weibo_check'),
    url(r'bind_email',views.bind_email,name='bind_email'),   #通过邮箱将第三方账户绑定到本站账号
]

辅助类

因为三种登录方式都遵循相似的处理流程,因此我们新建一个文件,将登录所需要的方法都封装到对应的类中,减少代码冗余,也方便理解。

import json
import urllib
import re

class OAuth_Base(object):    #基类,将相同的方法写入到此类中
    def __init__(self,client_id,client_key,redirect_url):   #初始化,载入对应的应用id、秘钥和回调地址
        self.client_id = client_id
        self.client_key = client_key
        self.redirect_url = redirect_url

    def _get(self,url,data):      #get方法
        request_url = '%s?%s' % (url,urllib.parse.urlencode(data))
        response = urllib.request.urlopen(request_url)
        return response.read()

    def _post(self,url,data):    #post方法
        request = urllib.request.Request(url,data = urllib.parse.urlencode(data).encode(encoding='UTF8'))     #1
        response = urllib.request.urlopen(request)
        return response.read()

#下面的方法,不同的登录平台会有细微差别,需要继承基类后重写方法

    def get_auth_url(self):   #获取code
        pass

    def get_access_token(self,code):   #获取access token
        pass

    def get_open_id(self):    #获取openid
        pass

    def get_user_info(self):   #获取用户信息
        pass

    def get_email(self):   #获取用户邮箱
        pass

#Github类
class OAuth_GITHUB(OAuth_Base):
    def get_auth_url(self):
        params = {
            'client_id':self.client_id,
            'response_type':'code',
            'redirect_uri':self.redirect_url,
            'scope':'user:email',
            'state':1
        }
        url = 'https://github.com/login/oauth/authorize?%s' % urllib.parse.urlencode(params)
        return url

    def get_access_token(self,code):
        params = {
            'grant_type':'authorization_code',
            'client_id':self.client_id,
            'client_secret':self.client_key,
            'code':code,
            'redirect_url':self.redirect_url
        }
        response = self._post('https://github.com/login/oauth/access_token',params)  #此处为post方法
        result = urllib.parse.parse_qs(response,True)
        self.access_token = result[b'access_token'][0]    
        return self.access_token

#github不需要获取openid,因此不需要get_open_id()方法

    def get_user_info(self):
        params ={'access_token':self.access_token}
        response = self._get('https://api.github.com/user',params)
        result = json.loads(response.decode('utf-8'))    
        self.openid = result.get('id','')
        return result

    def get_email(self):
        params ={'access_token':self.access_token}
        response = self._get('https://api.github.com/user/emails',params)
        result = json.loads(response.decode('utf-8'))
        return result[0]['email']

#QQ类    
class OAuth_QQ(OAuth_Base):
    def get_auth_url(self):
        params = {
            'client_id':self.client_id,
            'response_type':'code',
            'redirect_uri':self.redirect_url,
            'scope':'get_user_info',
            'state':1
        }
        url = 'https://graph.qq.com/oauth2.0/authorize?%s' % urllib.parse.urlencode(params)
        return url

    def get_access_token(self,code):
        params = {
            'grant_type':'authorization_code',
            'client_id':self.client_id,
            'client_secret':self.client_key,
            'code':code,
            'redirect_uri':self.redirect_url
        }
        response = self._get('https://graph.qq.com/oauth2.0/token',params)
        result = urllib.parse.parse_qs(response,True)
        self.access_token = result[b'access_token'][0]      
        return self.access_token

    def get_open_id(self):
        params ={'access_token':self.access_token}
        response = self._get('https://graph.qq.com/oauth2.0/me',params)
        response = re.split("[()]",response.decode('utf-8'))[1]   #将回应中的callback前缀去掉
        result = json.loads(response)   
        self.openid = result.get('openid','')
        return self.openid

    def get_user_info(self):
        params ={
            'access_token':self.access_token,
            'openid':self.openid,   
            'oauth_consumer_key':self.client_id,
        }
        response = self._get('https://graph.qq.com/user/get_user_info',params)
        result = json.loads(response.decode('utf-8'))    
        return result

#因为QQ没有开放获取qq邮箱的接口,因此不需要重写get_email()

#微博类
class OAuth_WEIBO(OAuth_Base):
    def get_auth_url(self):
        params = {
            'client_id':self.client_id,
            'response_type':'code',
            'redirect_uri':self.redirect_url,
            'scope':'email',
            'state':1
        }
        url = 'https://api.weibo.com/oauth2/authorize?%s' % urllib.parse.urlencode(params)
        return url

    def get_access_token(self,code):
        params = {
            'grant_type':'authorization_code',
            'client_id':self.client_id,
            'client_secret':self.client_key,
            'code':code,
            'redirect_uri':self.redirect_url
        }
        response = self._post('https://api.weibo.com/oauth2/access_token',params)
        result = json.loads(response.decode('utf-8'))
        self.access_token = result["access_token"]
        self.openid = result["uid"]
        return self.access_token

    def get_open_id(self):    #新浪的openid在之前get_access_token()方法中已经获得
        return self.openid

    def get_user_info(self):
        params ={
            'access_token':self.access_token,
            'uid':self.openid,  
        }
        response = self._get('https://api.weibo.com/2/users/show.json',params)
        result = json.loads(response.decode('utf-8'))   
        return result

    def get_email(self):
        params ={'access_token':self.access_token}
        response = self._get('https://api.weibo.com/2/account/profile/email.json',params)
        result = json.loads(response.decode('utf-8'))
        return result[0]['email']

视图函数

from django.shortcuts import render,render_to_response
from django.http import HttpResponseRedirect
from django.conf import settings
from .oauth_client import OAuth_GITHUB,OAuth_QQ,OAuth_WEIBO
from .models import OAuth_ex
from django.contrib.auth import login as auth_login
from users.models import User
from django.core.urlresolvers import reverse
from .forms import BindEmail

import time,uuid

def git_login(request):   #获取code
    oauth_git = OAuth_GITHUB(settings.GITHUB_APP_ID,settings.GITHUB_KEY,settings.GITHUB_CALLBACK_URL)
    url = oauth_git.get_auth_url()
    return HttpResponseRedirect(url)

def git_check(request):
    type='1'
    request_code = request.GET.get('code')
    oauth_git = OAuth_GITHUB(settings.GITHUB_APP_ID,settings.GITHUB_KEY,settings.GITHUB_CALLBACK_URL)
    try:
        access_token = oauth_git.get_access_token(request_code)   #获取access token
        time.sleep(0.1)    #此处需要休息一下,避免发送urlopen的10060错误
    except:  #获取令牌失败,反馈失败信息
        data={}
        data['goto_url'] = '/'
        data['goto_time'] = 10000
        data['goto_page'] = True
        data['message_title'] = '登录失败'
        data['message'] = '获取授权失败,请确认是否允许授权,并重试。若问题无法解决,请联系网站管理人员'
        return render_to_response('oauth/response.html',data)
    infos = oauth_git.get_user_info()   #获取用户信息
    nickname = infos.get('login','')
    image_url = infos.get('avatar_url','')
    open_id = str(oauth_git.openid)
    signature = infos.get('bio','')
    if not signature:
        signature = "无个性签名"
    sex = '1'
    githubs = OAuth_ex.objects.filter(openid=open_id,type=type)   #查询是否该第三方账户已绑定本网站账号
    if githubs:   #若已绑定,直接登录
        auth_login(request,githubs[0].user,backend='django.contrib.auth.backends.ModelBackend')
        return HttpResponseRedirect('/')
    else:   #否则尝试获取用户邮箱用于绑定账号
        try:
            email = oauth_git.get_email()
        except:   #若获取失败,则跳转到绑定用户界面,让用户手动输入邮箱
            url = "%s?nickname=%s&openid=%s&type=%s&signature=%s&image_url=%s&sex=%s" % (reverse('oauth:bind_email'),nickname,open_id,type,signature,image_url,sex)
            return HttpResponseRedirect(url)
    users = User.objects.filter(email=email)   #若获取到邮箱,则查询是否存在本站用户
    if users:   #若存在,则直接绑定
        user = users[0]
    else:   #若不存在,则新建本站用户
        while User.objects.filter(username=nickname):   #防止用户名重复
            nickname = nickname + '*'
        user = User(username=nickname,email=email,sex=sex,signature=signature)
        pwd = str(uuid.uuid1())   #随机设置用户密码
        user.set_password(pwd)
        user.is_active = True
        user.download_image(image_url,nickname)   #下载用户头像图片
        user.save()
    oauth_ex = OAuth_ex(user = user,openid = open_id,type=type)
    oauth_ex.save()    #保存后登陆
    auth_login(request,user,backend='django.contrib.auth.backends.ModelBackend')
    data={}    #反馈登陆结果
    data['goto_url'] = '/'
    data['goto_time'] = 10000
    data['goto_page'] = True
    data['message_title'] = '绑定用户成功'
    data['message'] = u'绑定成功!您的用户名为:<b>%s</b>。您现在可以同时使用本站账号和此第三方账号登录本站了!' % nickname
    return render_to_response('oauth/response.html',data)

def qq_login(request):
    oauth_qq = OAuth_QQ(settings.QQ_APP_ID,settings.QQ_KEY,settings.QQ_CALLBACK_URL)
    url = oauth_qq.get_auth_url()
    return HttpResponseRedirect(url)

def qq_check(request):
    type = 2
    code = request.GET.get('code','')
    oauth_qq = OAuth_QQ(settings.QQ_APP_ID,settings.QQ_KEY,settings.QQ_CALLBACK_URL)
    try:
        access_token = oauth_qq.get_access_token(code)
        time.sleep(0.1)
    except:
        data={}
        data['goto_url'] = '/'
        data['goto_time'] = 10000
        data['goto_page'] = True
        data['message_title'] = '登录失败'
        data['message'] = '获取授权失败,请确认是否允许授权,并重试。若问题无法解决,请联系网站管理人员'
        return render_to_response('oauth/response.html',data)
    openid = oauth_qq.get_open_id()
    qqs = OAuth_ex.objects.filter(openid=openid,type=type)
    if qqs:
        auth_login(request,qqs[0].user,backend='django.contrib.auth.backends.ModelBackend')
        return HttpResponseRedirect('/')
    else:
        infos = oauth_qq.get_user_info()
        nickname = infos.get('nickname','')
        image_url = infos.get('figureurl_qq_1','')
        sex = '1' if infos.get('gender','') == '男' else '2'
        signature = '无个性签名'
        url = "%s?nickname=%s&openid=%s&type=%s&signature=%s&image_url=%s&sex=%s" % (reverse('oauth:bind_email'),nickname,openid,type,signature,image_url,sex)
        return HttpResponseRedirect(url)

def weibo_login(request):
    oauth_weibo = OAuth_WEIBO(settings.WEIBO_APP_ID,settings.WEIBO_KEY,settings.WEIBO_CALLBACK_URL)
    url = oauth_weibo.get_auth_url()
    return HttpResponseRedirect(url)

def weibo_check(request):
    type = 3
    code = request.GET.get('code','')
    oauth_weibo = OAuth_WEIBO(settings.WEIBO_APP_ID,settings.WEIBO_KEY,settings.WEIBO_CALLBACK_URL)
    try:
        oauth_weibo.get_access_token(code)
        time.sleep(0.1)
    except:
        data={}
        data['goto_url'] = '/'
        data['goto_time'] = 10000
        data['goto_page'] = True
        data['message_title'] = '登录失败'
        data['message'] = '获取授权失败,请确认是否允许授权,并重试。若问题无法解决,请联系网站管理人员'
        return render_to_response('oauth/response.html',data)
    openid = oauth_weibo.get_open_id()
    weibos = OAuth_ex.objects.filter(openid=openid,type=type)
    if weibos:
        auth_login(request,weibos[0].user,backend='django.contrib.auth.backends.ModelBackend')
        return HttpResponseRedirect('/')
    else:
        try:
            email = oauth_weibo.get_email()
        except:
            infos = oauth_weibo.get_user_info()
            nickname = infos.get('screen_name','')
            image_url = infos.get('avatar_large','')
            signature = infos.get('description','')
            if not signature:
                signature = "无个性签名"
            print("signature="+signature)
            sex = '2' if infos.get('gender','') == 'f' else '1'
            url = "%s?nickname=%s&openid=%s&type=%s&signature=%s&image_url=%s&sex=%s" % (reverse('oauth:bind_email'),nickname,openid,type,signature,image_url,sex)
            return HttpResponseRedirect(url)
    users = User.objects.filter(email=email)
    if users:
        user = users[0]
    else:
        while User.objects.filter(username=nickname):
            nickname = nickname + '*'
        user = User(username=nickname,email=email,sex=sex,signature=signature)
        pwd = str(uuid.uuid1())
        user.set_password(pwd)
        user.is_active = True
        user.download_image(image_url,nickname)
        user.save()
    oauth_ex = OAuth_ex(user = user,openid = openid,type=type)
    oauth_ex.save()
    auth_login(request,user,backend='django.contrib.auth.backends.ModelBackend')
    data={}
    data['goto_url'] = '/'
    data['goto_time'] = 10000
    data['goto_page'] = True
    data['message_title'] = '绑定用户成功'
    data['message'] = u'绑定成功!您的用户名为:<b>%s</b>。您现在可以同时使用本站账号和此第三方账号登录本站了!' % nickname
    return render_to_response('oauth/response.html',data)   



def bind_email(request):   #使用户手动填写邮箱,绑定本站账号,因此需要一个BindEmail表单
    sex = request.GET.get('sex',request.POST.get('sex',''))
    openid = request.GET.get('openid',request.POST.get('openid',''))
    nickname = request.GET.get('nickname',request.POST.get('nickname',''))
    type = request.GET.get('type',request.POST.get('type',''))
    signature = request.GET.get('signature',request.POST.get('signature',''))
    image_url = request.GET.get('image_url',request.POST.get('image_url',''))
    if request.method == 'POST':
        form = BindEmail(request.POST)
        if form.is_valid():
            openid = form.cleaned_data['openid']
            nickname = form.cleaned_data['nickname']
            email = form.cleaned_data['email']
            password = form.cleaned_data['password']
            type = form.cleaned_data['type']
            signature = form.cleaned_data['signature']
            image_url = form.cleaned_data['image_url']
            sex = form.cleaned_data['sex']
            users = User.objects.filter(email = email)
            if users:
                user = users[0]
            else:
                while User.objects.filter(username=nickname):
                    nickname = nickname + '*'
                user = User(username=nickname,email=email,sex=sex,signature=signature)
                user.set_password(password)
                user.is_active = True
                user.download_image(image_url,nickname)
                user.save()
            oauth_ex = OAuth_ex(user=user,openid=openid,type=type)
            oauth_ex.save()
            auth_login(request,user,backend='django.contrib.auth.backends.ModelBackend')
            data={}
            data['goto_url'] = '/'
            data['goto_time'] = 10000
            data['goto_page'] = True
            data['message_title'] = '绑定账号成功'
            data['message'] = u'绑定成功!您的用户名为:<b>%s</b>。您现在可以同时使用本站账号和此第三方账号登录本站了!' % nickname
            return render_to_response('oauth/response.html',data)
    else:
        form = BindEmail(initial={
            'openid':openid,
            'nickname':nickname,
            'type':type,
            'signature':signature,
            'image_url':image_url,
            'sex':sex,
        })
    return render(request,'oauth/form.html',context={'form':form,'nickname':nickname,'type':type})

forms.py

from django import forms
from users.models import User
from django.core.exceptions import ValidationError
from django.contrib.auth import authenticate
from .models import OAuth_ex


class BindEmail(forms.Form):
    sex = forms.CharField(widget=forms.HiddenInput(attrs={'id':'sex'}))
    image_url = forms.CharField(widget=forms.HiddenInput(attrs={'id':'image_url'}),required=False)
    signature = forms.CharField(widget=forms.HiddenInput(attrs={'id':'signature'}),required=False)
    type = forms.CharField(widget=forms.HiddenInput(attrs={'id':'type'}))
    openid = forms.CharField(widget=forms.HiddenInput(attrs={'id':'openid'}))
    nickname = forms.CharField(widget=forms.HiddenInput(attrs={'id':'nickname'}))
    email = forms.EmailField(label=u'绑定邮箱',widget=forms.EmailInput(attrs={'class':'form-control','id':'email','placeholder':u'请输入用于绑定本站账号的邮箱','oninvalid':"setCustomValidity('请输入正确的邮箱地址');",'oninput':"setCustomValidity('');"}))
    password = forms.CharField(label=u'用户密码',widget=forms.PasswordInput(attrs={'class':'form-control','id':'password','placeholder':u'若尚未注册过本站账号,则该密码作为账户密码',"oninvalid":"setCustomValidity('请输入绑定用户的密码');",'oninput':"setCustomValidity('');"}))

    def clean_email(self):   #查询邮箱是否已经被绑定
        email = self.cleaned_data.get('email')
        type = self.cleaned_data.get('type')
        users = User.objects.filter(email=email)
        if users:
            if OAuth_ex.objects.filter(user=users[0],type=type):
                raise ValidationError(u'邮箱已经被绑定了')
        return email

    def clean_password(self):   #验证密码是否输入正确
        email = self.cleaned_data.get('email')
        password = self.cleaned_data.get('password')
        users = User.objects.filter(email=email)
        if users:
            user = authenticate(email=email,password=password)
            if user:
                return password
            else:
                raise ValidationError(u'密码不正确,绑定失败')
        return password

前端

登陆页面

<div id="oauth_button">
    <div class="pull-right">
        <span>其他账号登录:</span>
        <a href="{% url 'oauth:github_login' %}"><img class="type" src="{% static 'images/github.png' %}" alt="GITHUB" /></a> 
        <a href="{% url 'oauth:qq_login' %}"><img class="type" src="{% static 'images/tx.png' %}" alt="QQ" /></a> 
        <a href="{% url 'oauth:weibo_login' %}"><img class="type" src="{% static 'images/weibo.png' %}" alt="WEIBO" /></a>
    </div>
</div>

绑定用户页面

<div class="col-md-5 col-sm-6 col-xs-12">
    <div class="well">
        <h2>绑定账号</h2>
        <p>欢迎您登录:
        {% if type == '1' %}
            <img class="type" src="{% static 'images/github.png' %}" alt="GITHUB" />
        {% elif type == '2' %}
            <img class="type" src="{% static 'images/tx.png' %}" alt="QQ" />
        {% elif type == '3' %}
            <img class="type" src="{% static 'images/weibo.png' %}" alt="WEIBO" />
        {% endif %}
        <b>{{nickname}}</b>,请您绑定本站账号</p>
        <form role="form" action="{% url 'oauth:bind_email' %}" method="post">
            {% csrf_token %}
            {% for field in form %}
                {% if field.is_hidden %}
                    {{field}}
                {% else %}
                <div class="form-group">
                    {{ field.label_tag }}
                    {{ field }}
                    {{ field.errors }}
                </div>
                {% endif %}
            {% endfor %}
            {{ form.non_field_errors }}
            <div class="right">
                <button id="register" type="submit" class="btn btn-default pull-right">绑定</button>
            </div>
        </form>
    </div>
</div>

反馈信息页面

<body>
    <div class="container">
        <h2>{{message_title|safe}}</h2>
        <p class="text-center">
             {{message|safe}}
           </p>
        {% if goto_page %}
        <p class="text-center">
            本页面在 <b><span id="time_left"></span></b> 秒后自动跳转,若未跳转,请点击<a href="{{goto_url}}">此处</a>
           </p>
        {% endif %}
    </div>
    <script src="https://cdn.bootcss.com/jquery/2.1.1/jquery.min.js"></script>
    <script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
    <script src="{% static 'js/javascript.js' %}"></script>
    <script type="text/javascript">
        {% if goto_page %}   <!-- 自动页面跳转 -->
            $(function(){
                var time = {{goto_time}} / 1000;
                intervalid = window.setInterval(function(){
                    if (time <= 0){
                        clearInterval(intervalid);
                        window.location = '{{goto_url}}';
                    }
                    $('#time_left').text(time);
                    time -= 1;
                },1000);
            });
        {% endif %}
    </script>
</body>
posted @ 2019-03-20 19:36  桥前石头  阅读(974)  评论(0编辑  收藏  举报