关于本站
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 回复