OAuth(开放授权 Open Authorization)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。
处理流程包括:
开发之前,需要前往第三方登录的开发者平台QQ、新浪微博、Github,注册账号并填写信息申请接入,成功后会给你一个ID和秘钥,以后你就通过该ID和秘钥来获取令牌,从而实现第三方登录。申请ID和秘钥时Github不需要审核,很简单,而QQ和新浪微博需要审核,稍微麻烦一点。
#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' #填写你的回调地址
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)
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'绑定成功!您的用户名为:%s。您现在可以同时使用本站账号和此第三方账号登录本站了!' % 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'绑定成功!您的用户名为:%s。您现在可以同时使用本站账号和此第三方账号登录本站了!' % 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'绑定成功!您的用户名为:%s。您现在可以同时使用本站账号和此第三方账号登录本站了!' % 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})
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 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>
参考自http://yshblog.com/blog/60