关于本站
1、基于Django+Bootstrap开发
2、主要发表本人的技术原创博客
3、本站于 2015-12-01 开始建站
6月底的时候,我就想加上用QQ登录的功能。跑去QQ互联查看官方文档(一下被文档淹没了),注册了QQ互联、查了其他人写的代码、填了不少坑,终于写出来了。(工作也比较忙,实际开发差不多几天时间)。坑填好了,写出来,让更少人掉到坑里面。
一开始设想是用QQ登录之后,获取QQ邮箱。用这个QQ邮箱作为账号名登录或者注册。
在开发的过程中,发现获取不到QQ号,只能获取一个OpenID的东西。最后采取存储这个OpenID并绑定对应账号的方式。
所以需要创建对应的模型,即创建一个应用管理第三方登录。QQ登录功能开发流程如下图(结合你的网站设计可能不太一样):
第1步、QQ互联注册网站应用
打开QQ互联,进入管理中心。注册一下应用开发者,并添加网站应用。这里比较简单,需要注意的地方是网站信息。
网站地址需要验证一下,按照提示处理即可。
对于刚接触这个第三方登录的新手,最难理解的就是回调地址。回调地址不要填网站域名。
理解回调地址需要了解一下OAuth协议。
在你的网站页面里面,打开授权页面(这个授权页面不是回调地址)。在授权页面里面,登录QQ并确认授权。
授权之后,会得到一个授权码。回调地址就是用于接收这个授权码。
授权码以GET的方式返回,例如 http://yshblog.com/oauth/qq_check?code=xxxxxxxxxxxxx
通过这种方式,可以获取授权码,所以需要提供一个地址。这个地址先写一个暂时没有的地址,后面开发的时候,再给这个地址写对应的响应方法。
第2步、放置QQ按钮
这个QQ按钮是提供QQ登录的入口。从腾讯提供的QQ按钮下载放到你的登录页面即可。
不用看帮助文档里面的什么前端代码。这些用不上,只需要加一个固定的访问链接,再重定向即可。
我前端页面此处的代码如下:
<div> <span>其它账号登录:</span> <a href="{% url 'qq_login' %}" title="QQ登录"> <img src="/static/img/connect/logo_qq.png" alt="QQ登录"> </a> </div>
qq_login链接在下面第3步创建oauth应用里面设置。
第3步、创建oauth应用
怎么创建应用就不细说了,这是基本功。我创建的应用名为oauth,打算后面把微博登录、Github登录什么都放到这里。
创建完成之后,打开models.py文件,编写模型:
#coding:utf-8 from django.db import models from django.contrib.auth.models import User class OAuth_ex(models.Model): """User models ex""" user = models.ForeignKey(User) #和User关联的外键 qq_openid = models.CharField(max_length = 64) #QQ的关联OpenID def __unicode__(self): return u'<%s>' % (self.user)
该模型用于存储QQ登录返回的OpenID值。这个OpenID值是用QQ号一一对应。腾讯不给得到真实QQ号可能是出于保护隐私的考虑。
再打开urls.py文件,先写一下需要哪些链接地址:
from django.conf.urls import include, url #http://localhost:8000/oauth/ #start with 'oauth/' urlpatterns = [ url(r'qq_login', 'oauth.views.qq_login', name='qq_login'), url(r'qq_check', 'oauth.views.qq_check', name='qq_check'), url(r'bind_email', 'oauth.views.bind_email', name='bind_email'), ]
qq_login和qq_check前面有说到,分别是打开授权页面和回调地址。
bind_email是绑定邮箱的页面。
大致思路是授权之后,得到OpenID。判断这个OpenID是否存在数据库中。若存在,则直接登录对应的用户即可;若不存在,则打开这个绑定邮箱页面,绑定对应的用户。
同时,在总的urls路由中,加入这个应用路由。(总路由在和工程名一样的文件夹中的urls.py文件。我比较喜欢这种方式,对urls管理比较清晰)
urlpatterns = [ ... url(r'^oauth/',include('oauth.urls')), ]
这个路由控制,大家根据自己的工程自己写即可。
第4步、开发OAuth登录功能
该部分写的代码有点多,纠结怎么写博文才能比较好的表达式思路。先采取拆散代码讲解,最后再给出完整代码。
为了管理好OAuth,在oauth应用的文件夹下创建oauth_client.py文件。把相关的OAuth操作方法集成在一起。编辑oauth_client.py文件:
#coding:utf-8 import json import urllib, urllib2, urlparse class OAuth_QQ(): def __init__(self, client_id, client_key, redirect_uri): self.client_id = client_id self.client_key = client_key self.redirect_uri = redirect_uri def get_auth_url(self): """获取授权页面的网址""" params = {'client_id': self.client_id, 'response_type': 'code', 'redirect_uri': self.redirect_uri, 'scope': 'get_user_info', 'state': 1} url = 'https://graph.qq.com/oauth2.0/authorize?%s' % urllib.urlencode(params) return url
创建一个类,需要申请QQ登录的APP_ID、APP_KEY和回调地址。这些都是固定的,我把这几个常量放入到settings.py中。settings.py添加如下常量,具体的值请在你的申请页面查找:
#OAuth设置 QQ_APP_ID = 'xxxxx' QQ_KEY = 'xxxxxxxxxxxxxxxxxx' QQ_RECALL_URL = 'http://yshblog.com/oauth/qq_check'
回到OAuth_QQ类,现里面有个get_auth_url方法。该方法是获取打开授权页面的链接地址。(可参考官方帮助,写得不够清晰)
接着,在编辑oauth应用的views.py文件,加入qq_login对应的响应方法:
#coding:utf-8 from django.http import HttpResponseRedirect from django.conf import settings from oauth_client import OAuth_QQ #http://yshblog.com/oauth/qq_login def qq_login(request): oauth_qq = OAuth_QQ(settings.QQ_APP_ID, settings.QQ_KEY, settings.QQ_RECALL_URL) #获取 得到Authorization Code的地址 url = oauth_qq.get_auth_url() #重定向到授权页面 return HttpResponseRedirect(url)
到这里为止,就完成了点击QQ登录按钮,跳转到授权页面。
登录授权之后,授权页面会自动跳转到我们设置的回调地址。例如 http://yshblog.com/oauth/qq_check?code=xxxxxxxxxxxxx
我们可以获取这个地址上面的GET参数。先假设我们可以顺利获取到,继续完善OAuth_QQ类。拿到这个授权码之后,需要用该码获取腾讯的access_token通行令牌。(认证步骤有点多,好麻烦)
打开oauth_client.py文件,在OAuth_QQ类添加如下方法:
def get_access_token(self, code): """根据code获取access_token""" params = {'grant_type': 'authorization_code', 'client_id': self.client_id, 'client_secret': self.client_key, 'code': code, 'redirect_uri': self.redirect_uri} url = 'https://graph.qq.com/oauth2.0/token?%s' % urllib.urlencode(params) #访问该网址,获取access_token response = urllib2.urlopen(url).read() result = urlparse.parse_qs(response, True) access_token = str(result['access_token'][0]) self.access_token = access_token return access_token
该方法使用了urllib2,在服务器后台访问对应的链接,获取access_token,并返回该值。因为我后续不需要用access_token做其他动作,直接一次性获取QQ昵称和OpenID。所以不用记录这个通行令牌的有效期。
得到这个access_token之后,就可以做其他事了。首先需要获取授权用户的OpenID,本来这里我想获取QQ号的,而腾讯不允许。只好退而求次,获取并保存OpenID。可参考官方文档。
继续给这个OAuth_QQ添加获取OpenID的方法和使用OpenID获取QQ基本信息的方法:
def get_open_id(self): """获取QQ的OpenID""" params = {'access_token': self.access_token} url = 'https://graph.qq.com/oauth2.0/me?%s' % urllib.urlencode(params) response = urllib2.urlopen(url).read() v_str = str(response)[9:-3] #去掉callback的字符 v_json = json.loads(v_str) openid = v_json['openid'] self.openid = openid return openid def get_qq_info(self): """获取QQ用户的资料信息""" params = {'access_token': self.access_token, 'oauth_consumer_key': self.client_id, 'openid': self.openid} url = 'https://graph.qq.com/user/get_user_info?%s' % urllib.urlencode(params) response = urllib2.urlopen(url).read() return json.loads(response)
这里有一个坑,腾讯返回OpenID和QQ基本信息的内容格式都不一样。
再回头编辑views.py,添加回调地址的处理方法:
#coding:utf-8 from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse #url逆向解析 from django.contrib.auth.models import User from django.contrib.auth import logout,authenticate,login from django.conf import settings from oauth_client import OAuth_QQ from oauth.models import OAuth_ex from oauth.forms import BindEmail import time def qq_check(request): """登录之后,会跳转到这里。需要判断code和state""" request_code = request.GET.get('code') oauth_qq = OAuth_QQ(settings.QQ_APP_ID, settings.QQ_KEY, settings.QQ_RECALL_URL) #获取access_token access_token = oauth_qq.get_access_token(request_code) time.sleep(0.05) #稍微休息一下,避免发送urlopen的10060错误 open_id = oauth_qq.get_open_id() #检查open_id是否存在 qqs = OAuth_ex.objects.filter(qq_openid = open_id) if qqs: #存在则获取对应的用户,并登录 user = qqs[0].user #设置backend,绕开authenticate验证 setattr(user, 'backend', 'django.contrib.auth.backends.ModelBackend') login(request, user) return HttpResponseRedirect('/') else: #不存在,则跳转到绑定邮箱页面 infos = oauth_qq.get_qq_info() #获取用户信息 url = '%s?open_id=%s&nickname=%s' % (reverse('bind_email'), open_id, infos['nickname']) return HttpResponseRedirect(url)
这里有两个坑在等着大家。
按照思路,授权之后,调整到处理授权结果的页面。获取授权码之后,用get_access_token方法得到access_token。
再用access_token获取OpenID。第一个坑出现了,若不加time.sleep(0.05)休息一下的话,会得到urlopen 10060错误。
获取到open_id之后,再判断一下数据库中是否存在。若存在,则已经关联对应的用户了,直接登录该用户。
第二个坑跳出来了,login方法登录需要先进行authenticate方法验证。authenticate方法验证需要知道用户名和密码,而此处的密码是无法获取到明文密码。找了很多资料,发现authenticate方法得到的user对象和普通的user对象多了一个backend参数。手动设置一下这个参数,则可以顺利使用login方法登录该用户。
若open_id不存在,则跳转到绑定邮箱的页面。该页面需要知道open_id和QQ昵称(为什么需要QQ昵称,下一步会提到)。通过GET方式,把这两个参数写在链接上即可传递过去。
这里还需要提一下,本地调试的方法。因为授权之后是调整到部署之后的网站上,而部署的网站还没开发响应的代码,无法响应对应的地址。查到可以修改host方法进行本地调试,不过有点麻烦。可以这样:先本地打开授权页面授权,得到一个回调地址。回调地址上有授权码,如下图:
然后,复制授权码,手动写上对应的本地回调地址,替换其中的code参数值。如下图:
即可实现本地调试。
第5步、绑定用户
上面提到若open_id在数据库中不存在,则打开绑定用户页面。该页面我设计成form表单,在oauth应用下新建forms.py文件。如下代码:
#coding:utf-8 from django import forms from django.core.exceptions import ValidationError from django.contrib.auth.models import User from django.contrib.auth import authenticate from oauth.models import OAuth_ex class BindEmail(forms.Form): """bind the openid to email""" qq_openid = forms.CharField(widget=forms.HiddenInput(attrs={'id':'qq_openid'})) qq_nickname = forms.CharField(widget=forms.HiddenInput(attrs={'id':'qq_nickname'})) email = forms.EmailField(label=u'注册邮箱', widget=forms.EmailInput(attrs={'class':'form-control', 'id':'email','placeholder':u'请输入您注册用的邮箱'})) pwd = forms.CharField(label=u'用户密码', max_length=36, widget=forms.PasswordInput(attrs={'class':'form-control', 'id':'pwd','placeholder':u'若尚未注册过,该密码则作为用户密码'})) #验证邮箱 def clean_email(self): qq_openid = self.cleaned_data.get('qq_openid') email = self.cleaned_data.get('email') users = User.objects.filter(email = email) if users: #判断是否被绑定了 if OAuth_ex.objects.filter(user = users[0]): raise ValidationError(u'该邮箱已经被绑定了') return email #验证密码 def clean_pwd(self): email = self.cleaned_data.get('email') pwd = self.cleaned_data.get('pwd') users = User.objects.filter(email = email) if users: #若用户存在,判断密码是否正确 user = authenticate(username=email, password=pwd) if user is not None: return pwd else: return ValidationError(u'密码不正确,不能绑定')
该表单有4个字段。open_id和QQ昵称用hidden字段隐藏。另外邮箱和密码是验证对应用户的。
接着,在views.py继续编辑,添加表单处理的对应方法:
#coding:utf-8 from django.http import HttpResponse,HttpResponseRedirect from django.shortcuts import render_to_response, render from django.core.urlresolvers import reverse #url逆向解析 from django.contrib.auth.models import User from django.contrib.auth import logout,authenticate,login from django.conf import settings from oauth_client import OAuth_QQ from oauth.models import OAuth_ex from oauth.forms import BindEmail from user_ex.views import get_active_code, send_active_email import time def bind_email(request): open_id = request.GET.get('open_id') nickname = request.GET.get('nickname') data = {} data['form_title'] = u'绑定用户' data['submit_name'] = u' 确定 ' data['form_tip'] = u'Hi, <span class="label label-info"><img src="/static/img/connect/logo_qq.png">%s</span>!您已登录。请绑定用户,完成QQ登录' % nickname if request.method == 'POST': #表单提交 form = BindEmail(request.POST) #验证是否合法 if form.is_valid(): #判断邮箱是否注册了 qq_openid = form.cleaned_data['qq_openid'] qq_nickname = form.cleaned_data['qq_nickname'] email = form.cleaned_data['email'] pwd = form.cleaned_data['pwd'] users = User.objects.filter(email = email) if users: #用户存在,则直接绑定 user = users[0] if not user.first_name: user.first_name = qq_nickname #更新昵称 user.save() data['message'] = u'绑定账号成功,绑定到%s”' % email else: #用户不存在,则注册,并发送激活邮件 user=User(username=email, email=email) user.first_name = qq_nickname #使用QQ昵称作为昵称 user.set_password(pwd) user.is_active=False user.save() #发送激活邮件 try: active_code=get_active_code(email) send_active_email(email,active_code) except: pass data['message'] = u'绑定账号成功,绑定到%s。<br>并发送激活邮件到该邮箱”' % email #绑定用户并 oauth_ex = OAuth_ex(user = user, qq_openid = qq_openid) oauth_ex.save() #登录用户 user = authenticate(username=email, password=pwd) if user is not None: login(request, user) #页面提示 data['goto_url'] = '/' data['goto_time'] = 3000 data['goto_page'] = True return render_to_response('message.html',data) else: #正常加载 form = BindEmail(initial={ 'qq_openid': open_id, 'qq_nickname': nickname, }) data['form'] = form return render(request, 'form.html', data)
此处的处理逻辑是判断邮箱是否注册了,若注册了就判断密码是否正确,正确就绑定;不正确就提示。
若没有注册,则直接使用邮箱和密码注册用户,发送激活码。(发送激活邮件可参考《我的网站搭建(第15天) 注册认证》)
注册的时候,我是采用邮箱作为账号。昵称可以直接使用QQ昵称。当然这里还需要把QQ昵称显示出来,要不然申请QQ登录审核通不过。
这里使用到两个模版form.html和message.html。
form.html是通用的表单模版:
{% extends "base.html" %} {% block title %}杨仕航的博客{% endblock %} {% block content %} {#通用表单页面#} <div class="row"> <div class="col-md-6 col-md-offset-3" > <div id="panel_form" class="panel panel-default"> <div class="panel-body"> <h3 class="form_title">{{form_title}}</h3> <p>{{form_tip|safe}}</p> <form class="main_form" method='post'> {%csrf_token%} {% for field in form %} {# 区分是否是hidden字段 #} {% if field.is_hidden %} {{ field }} {% else %} <div class="input-group"> <label class="input-group-addon" for="{{ field.id_for_label }}"> {{ field.label }} </label> {{ field }} </div> {# 错误提示信息 #} <p class="text-danger text-right"> {{ field.errors.as_text }} </p> {%endif%} {%endfor%} <div class="text-right"> <input class="btn btn-primary" id="btn_submit" type="submit" value="{{submit_name}}"/> </div> </form> </div> </div> </div> </div> {% endblock %} {% block extra_footer %} <style type="text/css"> .form_title{ margin-bottom: 1em; padding-bottom: 0.5em; border-bottom: 1px #ccc solid; } .main_form div{ margin-top:1em; } #btn_submit{ margin-top: 1em; } #panel_form{ margin-top: 2em; margin-bottom: 3em; } </style> {% endblock %}
message.html是显示消息用的模版:
{% extends "base.html" %} {% block title %}杨仕航的博客{% endblock %} {% block content %} {#显示消息的页面#} <div class="row"> <center><h4 style="line-height:300%;">{{message|safe}}</h4></center> </div> {% endblock %} {% block extra_footer %} <script type="text/javascript"> {#页面调整控制#} {% if goto_page %} $(function(){ window.setTimeout(function(){ window.location = '{{goto_url}}'; },{{goto_time}}); }); {% endif %} </script> {% endblock %}
绑定用户的界面如下:
写完代码之后,本地测试可以通过。最后再部署到服务器并在QQ互联提交审核。一般审核要1~2天左右。若审核不通过,又不明白审核说明,就直接找客服问问。
-----<我是分割线,下面是view.py和oauth_client.py完整的代码>-----
views.py:
#coding:utf-8 from django.http import HttpResponse,HttpResponseRedirect from django.shortcuts import render_to_response, render from django.core.urlresolvers import reverse #url逆向解析 from django.contrib.auth.models import User from django.contrib.auth import logout,authenticate,login from django.conf import settings from oauth_client import OAuth_QQ from oauth.models import OAuth_ex from oauth.forms import BindEmail from user_ex.views import get_active_code, send_active_email import time #http://yshblog.com/oauth/qq_login def qq_login(request): oauth_qq = OAuth_QQ(settings.QQ_APP_ID, settings.QQ_KEY, settings.QQ_RECALL_URL) #获取 得到Authorization Code的地址 url = oauth_qq.get_auth_url() #重定向到授权页面 return HttpResponseRedirect(url) def qq_check(request): """登录之后,会跳转到这里。需要判断code和state""" request_code = request.GET.get('code') oauth_qq = OAuth_QQ(settings.QQ_APP_ID, settings.QQ_KEY, settings.QQ_RECALL_URL) #获取access_token access_token = oauth_qq.get_access_token(request_code) time.sleep(0.05) #稍微休息一下,避免发送urlopen的10060错误 open_id = oauth_qq.get_open_id() #检查open_id是否存在 qqs = OAuth_ex.objects.filter(qq_openid = open_id) if qqs: #存在则获取对应的用户,并登录 user = qqs[0].user #设置backend,绕开authenticate验证 setattr(user, 'backend', 'django.contrib.auth.backends.ModelBackend') login(request, user) return HttpResponseRedirect('/') else: #不存在,则跳转到绑定邮箱页面 infos = oauth_qq.get_qq_info() #获取用户信息 url = '%s?open_id=%s&nickname=%s' % (reverse('bind_email'), open_id, infos['nickname']) return HttpResponseRedirect(url) def bind_email(request): open_id = request.GET.get('open_id') nickname = request.GET.get('nickname') data = {} data['form_title'] = u'绑定用户' data['submit_name'] = u' 确定 ' data['form_tip'] = u'Hi, <span class="label label-info"><img src="/static/img/connect/logo_qq.png">%s</span>!您已登录。请绑定用户,完成QQ登录' % nickname if request.method == 'POST': #表单提交 form = BindEmail(request.POST) #验证是否合法 if form.is_valid(): #判断邮箱是否注册了 qq_openid = form.cleaned_data['qq_openid'] qq_nickname = form.cleaned_data['qq_nickname'] email = form.cleaned_data['email'] pwd = form.cleaned_data['pwd'] users = User.objects.filter(email = email) if users: #用户存在,则直接绑定 user = users[0] if not user.first_name: user.first_name = qq_nickname #更新昵称 user.save() data['message'] = u'绑定账号成功,绑定到%s”' % email else: #用户不存在,则注册,并发送激活邮件 user=User(username=email, email=email) user.first_name = qq_nickname #使用QQ昵称作为昵称 user.set_password(pwd) user.is_active=False user.save() #发送激活邮件 try: active_code=get_active_code(email) send_active_email(email,active_code) except: pass data['message'] = u'绑定账号成功,绑定到%s。<br>并发送激活邮件到该邮箱”' % email #绑定用户并 oauth_ex = OAuth_ex(user = user, qq_openid = qq_openid) oauth_ex.save() #登录用户 user = authenticate(username=email, password=pwd) if user is not None: login(request, user) #页面提示 data['goto_url'] = '/' data['goto_time'] = 3000 data['goto_page'] = True return render_to_response('message.html',data) else: #正常加载 form = BindEmail(initial={ 'qq_openid': open_id, 'qq_nickname': nickname, }) data['form'] = form return render(request, 'form.html', data)
oauth_client.py:
#coding:utf-8 import json import urllib, urllib2, urlparse class OAuth_QQ(): def __init__(self, client_id, client_key, redirect_uri): self.client_id = client_id self.client_key = client_key self.redirect_uri = redirect_uri def get_auth_url(self): """获取授权页面的网址""" params = {'client_id': self.client_id, 'response_type': 'code', 'redirect_uri': self.redirect_uri, 'scope': 'get_user_info', 'state': 1} url = 'https://graph.qq.com/oauth2.0/authorize?%s' % urllib.urlencode(params) return url def get_access_token(self, code): """根据code获取access_token""" params = {'grant_type': 'authorization_code', 'client_id': self.client_id, 'client_secret': self.client_key, 'code': code, 'redirect_uri': self.redirect_uri} url = 'https://graph.qq.com/oauth2.0/token?%s' % urllib.urlencode(params) #访问该网址,获取access_token response = urllib2.urlopen(url).read() result = urlparse.parse_qs(response, True) access_token = str(result['access_token'][0]) self.access_token = access_token return access_token def get_open_id(self): """获取QQ的OpenID""" params = {'access_token': self.access_token} url = 'https://graph.qq.com/oauth2.0/me?%s' % urllib.urlencode(params) response = urllib2.urlopen(url).read() v_str = str(response)[9:-3] #去掉callback的字符 v_json = json.loads(v_str) openid = v_json['openid'] self.openid = openid return openid def get_qq_info(self): """获取QQ用户的资料信息""" params = {'access_token': self.access_token, 'oauth_consumer_key': self.client_id, 'openid': self.openid} url = 'https://graph.qq.com/user/get_user_info?%s' % urllib.urlencode(params) response = urllib2.urlopen(url).read() return json.loads(response)
杨仕航
接下来,再试试新浪微博和github的第三方登录
2016-07-27 14:22 回复