关于本站
1、基于Django+Bootstrap开发
2、主要发表本人的技术原创博客
3、本站于 2015-12-01 开始建站
上次开发了用QQ第三方登录的功能。这次要加入新浪微博登录功能。研究了新浪微博登录的文档,发现新浪微博的OAuth可以获取邮箱,我的用户系统是用邮箱作为账号的。若可以直接获取邮箱,就不用再去绑定用户了。这点是QQ没有的。
但新浪微博的申请流程很乱,绕来绕去,有些按钮还点击不了。可能是之前OAuth1.0升级到OAuth2.0的过程时,没有整理好。
另外,QQ和新浪的都是OAuth2.0,数据结构都是一样的。所以我打算重新整合一下这个功能。前面写的OAuth登录只是针对QQ的。
先打开微博开发平台,并登录你的新浪微博账号。若没有就注册一个。
这里还需要再进一步身份认证,注册成为开发者,需要真实用户信息和上传身份证(可以手机拍照)。这里敏感信息太多了,我就不截图了。(忘记是在哪里注册了,好像是先接入网站的时候,就会自动提醒。)
再点击导航上的“微链接”--> “网站接入”。
接下来就是根据提示,一步一步来,比较简单。先验证网站的所有权,就可以得到key和secret了。
这里可以看到身份验证提示。若前面没有认证,就可以在这里进行认证。
若key和secret没有及时记录,也可以在导航“我的应用”里面查得。
这里我想添加网站图标什么都找不到。最后发现是在提交审核的时候,可以上传和填写。这里我建议试着提交审核,把这些东西都填好,免得后面还来找哪里填写。点击提交审核,跳到如下界面:
这里还需要一个备案号。如果网站的服务器在国内的话,就需要备案。若是服务器在国外。里面提示说什么需要服务器运营商提供证明。额……完全不用。我在一个问答社区找到可以用http://ip.chainaz.com的查询结果截图也可。具体看这个链接http://ask.oauth2.cn/show-40.html。
提交申请后,还需要一个东西:设置回调地址。这个在开发文档没有找着,是后面发现还是需要设置。在“我的应用”打开网站信息。找到这个地方,填写回调地址即可。
好了,万事俱备只欠代码。接下来就可以安安心心地开发了。具体的OAuth开发可以查看新浪微博的帮助文档。另外新浪微博提供了一个API测试页面。在帮助文档里面的css样式有问题,挡住了链接,无法点击。
OAuth的认证步骤我就不再详细讲解了,具体可以看看我前面写的QQ第三方登录博文。我这里就直接进入主题。
我前面为OAuth登录单独创建了一个应用,叫oauth。
在开发QQ第三方登录时,模型没有创建好,需要调整一下。models.py调整如下:
#coding:utf-8 from django.db import models from django.contrib.auth.models import User class OAuth_type(models.Model): """oauth type""" type_name = models.CharField(max_length = 12) title = models.CharField(max_length = 12) img = models.FileField(upload_to='static/img/connect') def __unicode__(self): return self.type_name class OAuth_ex(models.Model): """User models ex""" user = models.ForeignKey(User) #和User关联的外键 openid = models.CharField(max_length = 64, default = '') oauth_type = models.ForeignKey(OAuth_type, default=1) #关联账号的类型 def __unicode__(self): return u'<%s>' % (self.user)
之前的OAuth_ex模型原本有个字段叫qq_openid。这里为了兼容新浪微博,改名叫openid。并加入OAuth类型的外键字段。调整这个模型之后,同步更新一下数据库。
这里还需手动添加OAuth类型的数据,所以需要修改该应用的admin.py文件:
#coding:utf-8 from django.contrib import admin from apps_project.oauth.models import OAuth_ex, OAuth_type # Register your models here. class OAuthTypeAdmin(admin.ModelAdmin): list_display=('id','type_name', 'title', 'img') class OAuthAdmin(admin.ModelAdmin): list_display=('id', 'user', 'openid','oauth_type') admin.site.register(OAuth_ex, OAuthAdmin) admin.site.register(OAuth_type, OAuthTypeAdmin)
注意,我的应用都是放在apps_project文件夹里面。具体可以参考:Django目录优化。
再进入后台管理界面。添加两条记录:
这里有需要两个logo,懒得找的话,可以用用我的:
到了这里,我把所有OAuth需要的信息都作为配置信息写入到settings.py文件中:
#OAuth设置 OAUTH_QQ_CONFIG = { #OAuth_type对应数据 'oauth_type_id' : 1, #该值是id值和表oauth_type对应 'oauth_type' : 'QQ', #基本参数 'client_id' : '你的QQ第三方登录的Key', 'client_secret' : '你的QQ第三方登录的Secret', 'redirect_uri' : '你的QQ第三方登录的回调地址', 'scope' : 'get_user_info', 'state' : 'QQ', #用于标记是QQ的OAuth,目前无实际作用,可以用于记录登录之前的网址 #请求链接 'url_authorize' : 'https://graph.qq.com/oauth2.0/authorize', 'url_access_token' : 'https://graph.qq.com/oauth2.0/token', 'url_open_id' : 'https://graph.qq.com/oauth2.0/me', 'url_user_info' : 'https://graph.qq.com/user/get_user_info', 'url_email' : '', } OAUTH_SINA_CONFIG = { 'oauth_type_id' : 2, 'oauth_type' : 'Sina', 'client_id' : '你的新浪微博第三方登录的Key', 'client_secret' : '你的新浪微博第三方登录的Secret', 'redirect_uri' : '你的新浪微博第三方登录的回调地址', 'scope' : 'email', #获取邮箱接口需要的参数 'state' : 'Sina', 'url_authorize' : 'https://api.weibo.com/oauth2/authorize', 'url_access_token' : 'https://api.weibo.com/oauth2/access_token', 'url_open_id' : '', 'url_user_info' : 'https://api.weibo.com/2/users/show.json', 'url_email' : 'https://api.weibo.com/2/account/profile/email.json', }
同样,记得修改urls.py设置。我的设置如下,你的对应修改一下:
from django.conf.urls import include, url from apps_project.oauth import views as oauth_views #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'sina_login', oauth_views.sina_login, name='sina_login'), url(r'sina_check', oauth_views.sina_check, name='sina_check'), url(r'bind_email', oauth_views.bind_email, name='bind_email'), ]
前面的QQ第三方登录博文提到,把OAuth相关的功能都单独封装成一个类。由于QQ和新浪微博的第三方登录很多代码比较相似。于是我就把相同的部分集成为一个基类。然后再继承成为新的类。
先看看基类:
#coding:utf-8 import json import urllib, urllib2, urlparse import re class OAuth_Base(): def __init__(self, kw): #构造函数,传入一个配置字典。该字典已经在settings.py文件写好了 self.oauth_type_id = kw.get('oauth_type_id', 0) self.oauth_type = kw.get('oauth_type', '') self.client_id = kw.get('client_id', '') self.client_secret = kw.get('client_secret', '') self.redirect_uri = kw.get('redirect_uri', '') self.scope = kw.get('scope', '') self.state = kw.get('state', '') self.url_authorize = kw.get('url_authorize', '') self.url_access_token = kw.get('url_access_token', '') self.url_open_id = kw.get('url_open_id', '') self.url_user_info = kw.get('url_user_info', '') self.url_email = kw.get('url_email', '') def _get(self, url, data): """get请求""" request_url = '%s?%s' % (url, urllib.urlencode(data)) response = urllib2.urlopen(request_url) return response.read() def _post(self, url, data): """post请求""" request = urllib2.Request(url, data = urllib.urlencode(data)) response = urllib2.urlopen(request) #response = urllib2.urlopen(url, urllib.urlencode(data)) return response.read() #根据情况重写以下方法 def get_auth_url(self): """获取授权页面的网址""" params = {'client_id': self.client_id, 'response_type': 'code', 'redirect_uri': self.redirect_uri, 'scope': self.scope, 'state': self.state} return '%s?%s' % (self.url_authorize, urllib.urlencode(params)) def get_access_token(self, code): """根据code获取access_token""" pass def get_open_id(self): """获取用户的标识ID""" pass def get_user_info(self): pass def get_email(self): pass
通用的方法都写好了。不一样的方法再继承之后,重写即可。
这是QQ的OAuth类:
class OAuth_QQ(OAuth_Base): def get_access_token(self, code): """根据code获取access_token""" params = {'grant_type': 'authorization_code', 'client_id': self.client_id, 'client_secret': self.client_secret, 'code': code, 'redirect_uri': self.redirect_uri} response = self._get(self.url_access_token, params) #解析结果 if response[:8] == 'callback': v_str = str(response)[9:-3] #去掉callback的字符 v_json = json.loads(v_str) raise Exception(v_json['error_description']) else: result = urlparse.parse_qs(response, True) self.access_token = str(result['access_token'][0]) return self.access_token def get_open_id(self): """获取QQ的OpenID""" params = {'access_token': self.access_token} response = self._get(self.url_open_id, params) #去掉callback的字符 result = json.loads(str(response)[9:-3] ) self.openid = result['openid'] return self.openid def get_user_info(self): """获取QQ用户的资料信息""" params = {'access_token': self.access_token, 'oauth_consumer_key': self.client_id, 'openid': self.openid} response = self._get(self.url_user_info, params) return json.loads(response) def get_email(self): """获取邮箱""" #QQ没有提供获取邮箱的方法 raise Exception('can not get email')
注意,这里QQ无法获取邮箱,所以重写的get_email方法直接抛出错误。
这是新浪微博的OAuth类:
class OAuth_Sina(OAuth_Base): def get_access_token(self, code): """根据code获取access_token""" params = {'grant_type': 'authorization_code', 'client_id': self.client_id, 'client_secret': self.client_secret, 'code': code, 'redirect_uri': self.redirect_uri} #新浪微博此处是POST请求,返回JSON response = self._post(self.url_access_token, params) result = json.loads(response) self.access_token = result["access_token"] self.openid = result['uid'] return self.access_token def get_open_id(self): """获取Sina的uid,由于登录时就获取了,直接返回即可""" return self.openid def get_user_info(self): """获取用户资料信息""" params = {'access_token': self.access_token, 'uid': self.openid} response = self._get(self.url_user_info, params) return json.loads(response) def get_email(self): """获取邮箱""" #高级接口,需要申请 params = {"access_token": self.access_token} response = self._get(self.url_email, params) #return response #分析结果,获取邮箱成功返回是一个字典数组,而不成功则是一个字典 result = json.loads(response) #判断返回数据的类型 if isinstance(result, list): #获取并判断邮箱格式是否正确 email = result[0].get('email') pattern = re.compile(r'^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((\.[a-zA-Z0-9_-]{2,3}){1,2})$') match = pattern.match(email) if not match: raise Exception(u"email format error") return email elif isinstance(result, dict): raise Exception(result.get('error', 'get email happened error')) else: raise Exception('get email api error')
这两个类主要不同的地方在于数据请求的方法和返回结果的处理方法。
写好这两个类之后,再修改views.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 apps_project.oauth.oauth_client import OAuth_QQ, OAuth_Sina from apps_project.oauth.models import OAuth_ex, OAuth_type from apps_project.oauth.forms import BindEmail from apps_project.user_ex.views import get_active_code, send_active_email import time import uuid def _login_user(request, user): """直接登录用户""" #设置backend,绕开authenticate验证 setattr(user, 'backend', 'django.contrib.auth.backends.ModelBackend') login(request, user)
QQ第三方登录的处理方法和之前没什么变化,稍微调整了一下:
def qq_login(request): oauth_qq = OAuth_QQ(settings.OAUTH_QQ_CONFIG) #获取 得到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.OAUTH_QQ_CONFIG) #获取access_token try: access_token = oauth_qq.get_access_token(request_code) time.sleep(0.05) #稍微休息一下,避免发送urlopen的10060错误 open_id = oauth_qq.get_open_id() except Exception as e: data = {} data['message'] = u'登录出错,请稍后重试<br>(辅助信息%s)”' % e.message data['goto_url'] = '/' data['goto_time'] = 3000 data['goto_page'] = True return render_to_response('message.html', data) #检查open_id是否存在 qqs = OAuth_ex.objects.filter(openid = open_id, oauth_type = oauth_qq.oauth_type_id) if qqs: #存在则获取对应的用户,并登录 _login_user(request, qqs[0].user) return HttpResponseRedirect('/') else: #不存在,则跳转到绑定邮箱页面 infos = oauth_qq.get_user_info() #获取用户信息 url = '%s?open_id=%s&nickname=%s&oauth_type=%s' % ( reverse('bind_email'), open_id, infos['nickname'], oauth_qq.oauth_type) return HttpResponseRedirect(url)
新浪微博第三方登录的处理方法如下。这个方法和QQ的很相似,不同是的多了对可以直接获取到邮箱地址的处理代码。
后面如果加了其他第三方登录的,再看看如何整合一起,减少代码冗余:
def sina_login(request): oauth_sina = OAuth_Sina(settings.OAUTH_SINA_CONFIG) #获取 得到Authorization Code的地址 url = oauth_sina.get_auth_url() return HttpResponseRedirect(url) def sina_check(request): """登录之后,会跳转到这里。需要判断code和state""" request_code = request.GET.get('code') oauth_sina = OAuth_Sina(settings.OAUTH_SINA_CONFIG) #获取access_token try: access_token = oauth_sina.get_access_token(request_code) time.sleep(0.05) #稍微休息一下,避免发送urlopen的10060错误 open_id = oauth_sina.get_open_id() except Exception as e: data = {} data['message'] = u'登录出错,请稍后重试<br>(辅助信息%s)”' % str(e) data['goto_url'] = '/' data['goto_time'] = 3000 data['goto_page'] = True return render_to_response('message.html', data) #检查uid是否存在 sinas = OAuth_ex.objects.filter(openid = open_id, oauth_type = oauth_sina.oauth_type_id) if sinas: #存在则获取对应的用户,并登录 _login_user(request, sinas[0].user) return HttpResponseRedirect('/') else: #不存在,则尝试获取邮箱 try: #获取得到邮箱则直接绑定 email = oauth_sina.get_email() except Exception as e: #获取不到则跳转到邮箱绑定页面 #获取用户资料 infos = oauth_sina.get_user_info() nickname = infos['screen_name'] #还需要第三方账户的类型(oauth_type) 和 修改绑定邮箱的页面 url = '%s?open_id=%s&nickname=%s&oauth_type=%s' % ( reverse('bind_email'), open_id, nickname, oauth_sina.oauth_type) return HttpResponseRedirect(url) #判断是否存在对应的用户(我这里的用户名就是邮箱,根据你的实际情况参考) users = User.objects.filter(username = email) if users: #存在则绑定和登录 user = users[0] else: #不存在则直接注册并登录 user = User(username = email, email = email) pwd = str(uuid.uuid1()) #生成随机密码 user.set_password(pwd) user.is_active = True #真实邮箱来源,则认为是有效用户 user.save() #添加绑定记录 oauth_type = OAuth_type.objects.get(id = oauth_sina.oauth_type_id) oauth_ex = OAuth_ex(user = user, openid = open_id, oauth_type = oauth_type) oauth_ex.save() #更新昵称 if not user.first_name: user.first_name = nickname user.save() _login_user(request, user) data = {} data['goto_url'] = '/' data['goto_time'] = 3000 data['goto_page'] = True data['message'] = u'登录并绑定成功' return render_to_response('message.html', data)
绑定邮箱页面的代码也稍微改动了一些:
def bind_email(request): open_id = request.GET.get('open_id') nickname = request.GET.get('nickname') oauth_type = request.GET.get('oauth_type') data = {} #判断oauth类型 oauth_types = OAuth_type.objects.filter(type_name = oauth_type) if oauth_types.count() > 0: oauth_type = oauth_types[0] img_url = oauth_type.img else: data['goto_url'] = '/' data['goto_time'] = 3000 data['goto_page'] = True data['message'] = u'错误的登录类型,请检查' return render_to_response('message.html',data) data['form_title'] = u'绑定用户' data['submit_name'] = u' 确定 ' data['form_tip'] = u'Hi, <span class="label label-info"><img src="/%s">%s</span>!您已登录。请绑定用户,完成登录' % (img_url, nickname) 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'] pwd = form.cleaned_data['pwd'] users = User.objects.filter(email = email) if users: #用户存在,则直接绑定 user = users[0] if not user.first_name: user.first_name = nickname #更新昵称 user.save() data['message'] = u'绑定账号成功,绑定到%s”' % email else: #用户不存在,则注册,并发送激活邮件 user=User(username=email, email=email) user.first_name = nickname #使用昵称 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, openid = openid, oauth_type = oauth_type) 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={ 'openid': open_id, 'nickname': nickname, 'oauth_type_id': oauth_type.id, }) data['form'] = form return render(request, 'form.html', data)
加了一个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 apps_project.oauth.models import OAuth_ex, OAuth_type class BindEmail(forms.Form): """bind the openid to email""" openid = forms.CharField(widget=forms.HiddenInput(attrs={'id':'openid'})) nickname = forms.CharField(widget=forms.HiddenInput(attrs={'id':'nickname'})) oauth_type_id = forms.CharField(widget=forms.HiddenInput(attrs={'id':'oauth_type'})) 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): oauth_type_id = self.cleaned_data.get('oauth_type_id') email = self.cleaned_data.get('email') users = User.objects.filter(email = email) oauth_type = OAuth_type.objects.get(id = oauth_type_id) if users: #判断是否被绑定了 if OAuth_ex.objects.filter(user = users[0], oauth_type = oauth_type): 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'密码不正确,不能绑定')
最后,再给登录页面加上登录按钮即可。
代码和篇幅有点多,可以结合一下我前面写的QQ第三方登录博文。
另外,获取邮箱地址的接口是属于高级权限,需要先通过审核,然后在我的应用中的接口管理中申请。