一、QQ登录:
QQ登录,亦即我们所说的第三方登录,是指用户可以不在本项目中输入密码,而直接通过第三方的验证,成功登录本项目。
若想实现QQ登录,需要成为QQ互联的开发者,审核通过才可实现。注册方法可参考链接http://wiki.connect.qq.com/%E6%88%90%E4%B8%BA%E5%BC%80%E5%8F%91%E8%80%85
成为QQ互联开发者后,还需创建应用,即获取本项目对应与QQ互联的应用ID,创建应用的方法参考链接http://wiki.connect.qq.com/__trashed-2
QQ登录开发文档连接http://wiki.connect.qq.com/%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C_oauth2-0
1.前期准备:
QQ开放平台:https://open.tencent.com/
- 1.注册成为""QQ开放平台"的的开发者;
- 2.创建网站应用,提交审核;
- 3.审核成功,拿到app_id和app_key
- 4.在前端页面中放置QQ图标;
- 5.获取Access_token;
- 6.获取open_id值(open_id是QQ用户身份的唯一标识)
2.创建模型类:
创建一个新的应用oauth,用来实现QQ第三方认证登录。总路由前缀 oauth/
在meiduo/meiduo_mall/utils/models.py文件中创建模型类基类,用于增加数据新建时间和更新时间。
# /meiduo_mall/utils/models.py
from django.db import models
class BaseModel(models.Model):
"""为模型类补充字段"""
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
abstract = True # 说明是抽象模型类, 用于继承使用,数据库迁移时不会创建BaseModel的表
将第三方账号登录功能单独放到一个子应用(oauth)中,
ubuntu:~/Desktop/meiduo/meiduo_mall/meiduo_mall/apps$ python ../../manage.py startapp oauth
在oauth/models.py中定义QQ身份(openid)和微博身份(weibotoken), 与用户模型类User的关联关系。
from django.db import models
from meiduo_mall.utils.models import BaseModel
class OAuthQQUser(BaseModel):
"""
QQ登录用户数据
"""
user = models.ForeignKey('users.User', on_delete=models.CASCADE, verbose_name='用户')
openid = models.CharField(max_length=64, verbose_name='openid', db_index=True) # QQ用户标识openid
weibotoken = models.CharField(max_length=64, verbose_name='weibotoken', db_index=True) # 微博用户登录Access_token
class Meta:
db_table = 'tb_oauth'
verbose_name = '第三方登录登录用户数据'
verbose_name_plural = verbose_name
进行数据库迁移
python manage.py makemigrations
python manage.py migrate
3.QQ登录SDK使用:
- QQ官方没有给出响应的python3版本的SDK, 所以, 根据QQ开放平台的官方文档手写一个QQ登录SDK。
- 除了手写,还可以使用OOLoginTool第三方模块
pip install QQLoginTool
4.自定义QQ登录的SDK:
# oauth/utils.py
import json
from urllib.parse import urlencode, parse_qs
import requests
from django.conf import settings
class OAuthQQ(object):
""" QQ认证辅助工具类 """
def __init__(self, client_id = None, client_secret = None, redirect_uri = None, state = None):
"""
初始化QQ登录对象
:param client_id:
:param client_secret:
:param redirect_uri:
:param state:
"""
self.client_id = client_id or settings.QQ_CLIENT_ID
self.client_secret = client_secret or settings.QQ_CLIENT_SECRET
self.redirect_uri = redirect_uri or settings.QQ_REDIRECT_URI
self.state = state or settings.QQ_STATE # 用于保存登录成功后的跳转页面路径
def get_qq_url(self):
"""
获取QQ登录链接
:return:
"""
data_dict = {
'response_type': 'code',
'client_id': self.client_id,
'redirect_uri': self.redirect_uri,
'state': self.state
}
# 构造QQ登录链接==>查询字符串
qq_url = 'https://graph.qq.com/oauth2.0/authorize?'+ urlencode(data_dict)
return qq_url
def get_access_token(self, code):
"""
获取access_token值
:param code:
:return:
"""
# 构建参数数据
data_dict = {
'grant_type': 'authorization_code',
'client_id': self.client_id,
'client_secret': self.client_secret,
'redirect_uri': self.redirect_uri,
'code': code
}
# 构建url
access_url = 'https://graph.qq.com/oauth2.0/token?' + urlencode(data_dict)
# 发送请求
try:
response = requests.get(access_url)
# 提取数据
# access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14
data = response.text
# 转化为字典
data = parse_qs(data)
except:
raise Exception('qq请求失败')
# 提取access_token
access_token = data.get('access_token', None)
if not access_token:
raise Exception('access_token获取失败')
return access_token[0]
def get_open_id(self, access_token):
"""
获取openid
:param access_token:
:return:
"""
# 构建请求url
url = "https://graph.qq.com/oauth2.0/me?access_token=" + access_token
# 发送请求
try:
response = requests.get(url)
# 提取数据
# callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
# code=asdasd&msg=asjdhui 错误的时候返回的结果
data = response.text
data = data[10:-3]
except:
raise Exception('qq请求失败')
# 转化为字典
try:
data_dict = json.loads(data)
# 获取openid
print("data:%s" % data_dict)
openid = data_dict.get('openid')
except:
raise Exception('openid获取失败')
return openid
5. 返回QQ登录网址的视图:
5.1 后端接口设计:
请求方式: GET /oauth/qq/authorization/
请求参数: 查询字符串
参数名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
next | str | 否 | 用户QQ登录成功后进入美多商城的哪个网址 |
返回数据: JSON
返回值 | 类型 | 是否必传 | 说明 |
---|---|---|---|
login_url | str | 是 | qq登录网址 |
{
"login_url": "https://graph.qq.com/oauth2.0/show?which=Login&display=pc&response_type=code&client_id=101474184&redirect_uri=http%3A%2F%2Fwww.meiduo.site%3A8080%2Foauth_callback.html&state=%2F&scope=get_user_info"
}
5.1.2准备配置信息:
在配置文件中添加关于QQ登录的应用开发信息
settings/dev.py
# QQ登录参数
QQ_CLIENT_ID = '10*****84'
QQ_CLIENT_SECRET = 'c6*******************37c'
QQ_REDIRECT_URI = 'http://www.meiduo.site:8080/oauth_callback.html'
5.1.3 后端的逻辑实现:
class QQAuthURLLView(APIView):
"""定义QQ第三方登录的视图类"""
def get(self, request):
"""
获取QQ登录的链接
:param request:
:return:
"""
# 1.通过查询字符串
next = request.query_params.get('state')
if not next:
next = "/"
# 获取QQ登录网页
oauth = OAuthQQ(client_id=settings.QQ_CLIENT_ID,
client_secret=settings.QQ_CLIENT_SECRET,
redirect_uri=settings.QQ_REDIRECT_URI,
state=next)
login_url = oauth.get_qq_url()
return Response({"login_url": login_url})
5.1.4 前端实现:
修改login.js,在methods中增加qq_login方法
// qq登录
qq_login: function(){
var next = this.get_query_string('next') || '/';
axios.get(this.host + '/oauth/qq/authorization/?next=' + next, {
responseType: 'json'
})
.then(response => {
location.href = response.data.login_url;
})
.catch(error => {
console.log(error.response.data);
})
}
6.OAuth2.0认证:
- 准备oauth_callback回调页,用于扫码后接受Authorization Code
- 通过Authorization Code获取Access Token
- 通过Access Token获取OpenID
6.1 oauth_callback回调页
用户在QQ登录成功后,QQ会将用户重定向回我们配置的回调callback网址,在本项目中,我们申请QQ登录开发资质时配置的回调地址为:
http://www.meiduo.site:8080/oauth_callback.html
我们在front_end_pc目录中新建oauth_callback.html文件,用于接收QQ登录成功的用户回调请求。在该页面中,提供了用于用户首次使用QQ登录时需要绑定用户身份的表单信息。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>美多商城-绑定用户</title>
<link rel="stylesheet" type="text/css" href="css/reset.css">
<link rel="stylesheet" type="text/css" href="css/main.css">
<script type="text/javascript" src="js/hosts.js"></script>
<script type="text/javascript" src="js/vue-2.5.16.js"></script>
<script type="text/javascript" src="js/axios-0.18.0.min.js"></script>
</head>
<body>
<div id="app">
<div v-if="is_show_waiting" class="pass_change_finish">请稍后...</div>
<div v-else>
<div class="register_con">
<div class="l_con fl">
<a class="reg_logo"><img src="images/logo.png"></a>
<div class="reg_slogan">商品美 · 种类多 · 欢迎光临</div>
<div class="reg_banner"></div>
</div>
<div class="r_con fr">
<div class="reg_title clearfix">
<h1>绑定用户</h1>
</div>
<div class="reg_form clearfix" v-cloak>
<form id="reg_form" v-on:submit.prevent="on_submit">
<ul>
<li>
<label>手机号:</label>
<input type="text" v-model="mobile" v-on:blur="check_phone" name="phone" id="phone">
<span v-show="error_phone" class="error_tip">{{ error_phone_message }}</span>
</li>
<li>
<label>密码:</label>
<input type="password" v-model="password" v-on:blur="check_pwd" name="pwd" id="pwd">
<span v-show="error_password" class="error_tip">密码最少8位,最长20位</span>
</li>
<li>
<label>短信验证码:</label>
<input type="text" v-model="sms_code" v-on:blur="check_sms_code" name="msg_code" id="msg_code" class="msg_input">
<a v-on:click="send_sms_code" class="get_msg_code">{{ sms_code_tip }}</a>
<span v-show="error_sms_code" class="error_tip">{{ error_sms_code_message }}</span>
</li>
<li class="reg_sub">
<input type="submit" value="保 存" name="">
</li>
</ul>
</form>
</div>
</div>
</div>
<div class="footer no-mp">
<div class="foot_link">
<a href="#">关于我们</a>
<span>|</span>
<a href="#">联系我们</a>
<span>|</span>
<a href="#">招聘人才</a>
<span>|</span>
<a href="#">友情链接</a>
</div>
<p>CopyRight © 2016 北京美多商业股份有限公司 All Rights Reserved</p>
<p>电话:010-****888 京ICP备*******8号</p>
</div>
</div>
</div>
<script type="text/javascript" src="js/oauth_callback.js"></script>
</body>
</html>
在js目录中新建oauth_callback.js文件
var vm = new Vue({
el: '#app',
data: {
host: host,
is_show_waiting: true,
error_password: false,
error_phone: false,
error_sms_code: false,
error_phone_message: '',
error_sms_code_message: '',
sms_code_tip: '获取短信验证码',
sending_flag: false, // 正在发送短信标志
password: '',
mobile: '',
sms_code: '',
access_token: ''
},
mounted: function(){
// 从路径中获取qq重定向返回的code
var code = this.get_query_string('code');
axios.get(this.host + '/oauth/qq/user/?code=' + code, {
responseType: 'json',
withCredentials: true
})
.then(response => {
if (response.data.user_id){
// 用户已绑定
sessionStorage.clear();
localStorage.clear();
localStorage.user_id = response.data.user_id;
localStorage.username = response.data.username;
localStorage.token = response.data.token;
// 登录成功后,根据state将用户引导到登录成功后的页面
var state = this.get_query_string('state');
location.href = state;
} else {
// 用户未绑定
this.access_token = response.data.access_token;
this.is_show_waiting = false;
}
})
.catch(error => {
console.log(error.response.data);
alert('服务器异常');
})
},
methods: {
// 获取url路径参数
get_query_string: function(name){
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
var r = window.location.search.substr(1).match(reg);
if (r != null) {
return decodeURI(r[2]);
}
return null;
},
check_pwd: function (){
var len = this.password.length;
if(len<8||len>20){
this.error_password = true;
} else {
this.error_password = false;
}
},
check_phone: function (){
var re = /^1[345789]\d{9}$/;
if(re.test(this.mobile)) {
this.error_phone = false;
} else {
this.error_phone_message = '您输入的手机号格式不正确';
this.error_phone = true;
}
},
check_sms_code: function(){
if(!this.sms_code){
this.error_sms_code_message = '请填写短信验证码';
this.error_sms_code = true;
} else {
this.error_sms_code = false;
}
},
// 发送手机短信验证码
send_sms_code: function(){
// 重新发送短信后,隐藏提示信息
this.error_sms_code = false;
if (this.sending_flag == true) {
return;
}
this.sending_flag = true;
// 校验参数,保证输入框有数据填写
this.check_phone();
if (this.error_phone == true) {
this.sending_flag = false;
return;
}
// 向后端接口发送请求,让后端发送短信验证码
axios.get(this.host + '/sms_codes/' + this.mobile + '/', {
responseType: 'json'
})
.then(response => {
// 表示后端发送短信成功
// 倒计时60秒,60秒后允许用户再次点击发送短信验证码的按钮
var num = 60;
// 设置一个计时器
var t = setInterval(() => {
if (num == 1) {
// 如果计时器到最后, 清除计时器对象
clearInterval(t);
// 将点击获取验证码的按钮展示的文本回复成原始文本
this.sms_code_tip = '获取短信验证码';
// 将点击按钮的onclick事件函数恢复回去
this.sending_flag = false;
} else {
num -= 1;
// 展示倒计时信息
this.sms_code_tip = num + '秒';
}
}, 1000, 60)
})
.catch(error => {
if (error.response.status == 400) {
// 展示发送短信错误提示
this.error_sms_code = true;
this.error_sms_code_message = error.response.data.message;
} else {
console.log(error.response.data);
}
this.sending_flag = false;
})
},
// 保存
on_submit: function(){
this.check_pwd();
this.check_phone();
this.check_sms_code();
if(this.error_password == false && this.error_phone == false && this.error_sms_code == false) {
axios.post(this.host + '/oauth/qq/user/', {
password: this.password,
mobile: this.mobile,
sms_code: this.sms_code,
access_token: this.access_token
}, {
responseType: 'json',
})
.then(response => {
// 记录用户登录状态
sessionStorage.clear();
localStorage.clear();
localStorage.token = response.data.token;
localStorage.user_id = response.data.user_id;
localStorage.username = response.data.username;
location.href = this.get_query_string('state');
})
.catch(error=> {
if (error.response.status == 400) {
this.error_sms_code_message = error.response.data.message;
this.error_sms_code = true;
} else {
console.log(error.response.data);
}
})
}
}
}
});
6.2 获取QQ用户的OpenID:
在QQ将用户重定向到此网页的时候,重定向的网址会携带QQ提供的code参数,用于获取用户信息使用,我们需要将这个code参数发送给后端,在后端中使用code参数向QQ请求用户的身份信息,并查询与该QQ用户绑定的用户。
6.2.1 后端接口:
** 请求方式:** GET oauth/qq/user/
** 请求参数:** 查询字符串
参数 | 类型 | 是否必传 | 说明 |
---|---|---|---|
code | str | 是 | qq返回的授权凭证code |
返回数据:
参数 | 类型 | 是否必传 | 说明 |
---|---|---|---|
access_token | str | 否 | 用户是第一次使用QQ登录时返回,其中包含openid,用于绑定身份使用,注意这个是我们自己生成的 |
token | str | 否 | 用户不是第一次使用QQ登录时返回,登录成功的JWT token |
username | str | 否 | 用户不是第一次使用QQ登录时返回,用户名 |
user_id | int | 否 | 用户不是第一次使用QQ登录时返回,用户id |
{
"access_token": xxxx,
}
或
{
"token": "xxx",
"username": "python",
"user_id": 1
}
6.2.2 后端逻辑实现:
# oauth/views.py
class QQAuthURLLView(APIView):
"""定义QQ第三方登录的视图类"""
def get(self, request):
"""
获取QQ登录的链接
:param request:
:return:
"""
# 1.通过查询字符串
next = request.query_params.get('state')
if not next:
next = "/"
# 获取QQ登录网页
oauth = OAuthQQ(client_id=settings.QQ_CLIENT_ID,
client_secret=settings.QQ_CLIENT_SECRET,
redirect_uri=settings.QQ_REDIRECT_URI,
state=next)
login_url = oauth.get_qq_url()
return Response({"login_url": login_url})
class QQOauthView(APIView):
"""验证QQ登录"""
def get(self,request):
"""
第三方登录检查
:param request:
:return:
"""
# 1. 获取code值
code = request.query_params.get("code")
# 2.检查参数
if not code:
return Response({'errors': '缺少code值'}, status=400)
# 3.通过code获取token值
state = '/'
# 创建qq对象
qq = OAuthQQ(client_id=settings.QQ_CLIENT_ID,
client_secret=settings.QQ_CLIENT_SECRET,
redirect_uri=settings.QQ_REDIRECT_URI,
state=state)
access_token = qq.get_access_token(code=code)
# print("access:%s" % access_token)
# 4. 获取openid值
openid = qq.get_open_id(access_token=access_token)
print("openid:%s" % openid)
# 5.判断是否绑定过美多账号
try:
qq_user = OAuthUser.objects.get(openid=openid)
except:
# 6.未绑定,进入绑定页面,完成绑定
tjs = TJS(settings.SECRET_KEY, 300)
open_id = tjs.dumps({'openid': openid}).decode()
return Response({'access_token': open_id})
else:
# 7.绑定过,则登录成功
# 生成jwt-token值
user = qq_user.user
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user) # 生成载荷部分
token = jwt_encode_handler(payload) # 生成token
response = Response(
{
'token': token,
'username': user.username,
'user_id': user.id
}
)
return response
7.补充:使用itsdangerous的使用
-
itsdangerous模块的参考资料连接http://itsdangerous.readthedocs.io/en/latest/
-
安装:pip install itsdangerous
-
TimedJSONWebSignatureSerializer的使用
- 使用TimedJSONWebSignatureSerializer可以生成带有有效期的token
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from django.conf import settings
# serializer = Serializer(秘钥, 有效期秒)
serializer = Serializer(settings.SECRET_KEY, 300)
# serializer.dumps(数据), 返回bytes类型
token = serializer.dumps({'mobile': '18512345678'})
token = token.decode()
# 检验token
# 验证失败,会抛出itsdangerous.BadData异常
serializer = Serializer(settings.SECRET_KEY, 300)
try:
data = serializer.loads(token)
except BadData:
return None
8. OpenID绑定美多商城用户
如果用户是首次使用QQ登录,则需要绑定用户,页面如下:
业务逻辑:
用户需要填写手机号、密码、图片验证码、短信验证码
如果用户未在美多商城注册过,则会将手机号作为用户名为用户创建一个美多账户,并绑定用户
如果用户已在美多商城注册过,则检验密码后直接绑定用户
1.1 1. 绑定QQ身份的处理流程
1.2 后端接口设计
请求方式: POST /oauth/qq/user/
请求参数: JSON 或 表单
参数 | 类型 | 是否必须 | 说明 |
---|---|---|---|
mobile | str | 是 | 手机号 |
password | str | 是 | 密码 |
sms_code | str | 是 | 短信验证码 |
access_token | str | 是 | 凭据 (包含openid) |
返回数据: JSON
返回值 | 类型 | 是否必须 | 说明 |
---|---|---|---|
token | str | 是 | JWT token |
id | int | 是 | 用户id |
username | str 是 用户名 |
1.3 后盾逻辑实现:
- 在oauth/views.py修改QQAuthUserView视图:
#-*-coding:utf-8-*-
class QQOauthView(APIView):
"""验证QQ登录"""
def get(self,request):
...
def post(self, request):
"""
QQ绑定页面的请求,完成绑定用户操作
:param request:
:return:
"""
# 1. 获取前端数据
data = request.data
# 2.调用序列化起验证数据
ser = QQOauthSerializers(data=data)
ser.is_valid()
print(ser.errors)
# 保存绑定数据
ser.save()
return Response(ser.data)
- 在oauth/serializers.py中定义序列化器:
# -*-coding:utf-8-*-
import re
from django.conf import settings
from rest_framework_jwt.serializers import serializers
from itsdangerous import TimedJSONWebSignatureSerializer as TJS
from rest_framework_jwt.settings import api_settings
from oauth.models import OAuthUser
from users.models import User
from verifications.views import SMSCodeView, ImageCheckViews
class QQOauthSerializers(serializers.ModelSerializer):
"""QQ用户绑定的序列化起"""
# 指名模型类中没有的字段
mobile = serializers.CharField(max_length=11)
sms_code = serializers.CharField(max_length=6, min_length=6, write_only=True)
access_token = serializers.CharField(write_only=True) # 反序列化输入
token = serializers.CharField(read_only=True)
user_id = serializers.IntegerField(read_only=True) # 序列化输出
class Meta:
model=User
fields=('password', 'mobile', 'username', 'sms_code', 'token','access_token', 'user_id')
extra_kwargs = {
'username':{
'read_only':True
},
'password':{
'write_only':True
}
}
def validated_mobile(self, value):
"""
验证手机号
:param value:
:return:
"""
if not re.match(r"1[3-9]\d{9}$", value):
raise serializers.ValidationError("手机号格式错误")
return value
def validate(self, attrs):
"""
验证access_token
:param attrs:
:return:
"""
tjs = TJS(settings.SECRET_KEY, 300)
try:
data = tjs.loads(attrs["access_token"]) # 解析token
except:
raise serializers.ValidationError("无效的token")
# 获取openid
openid = data.get("openid")
# attrs中添加openid
attrs["openid"] = openid
# 验证短信验证码:
rel_sms_code = SMSCodeView.checkSMSCode(attrs["mobile"])
if not rel_sms_code:
raise serializers.ValidationError('短信验证码失效')
# 3、比对用户输入的短信和redis中真实短信
if attrs['sms_code'] != rel_sms_code:
raise serializers.ValidationError('短信验证不一致')
# 验证手机号是否被注册过
try:
user = User.objects.get(mobile=attrs['mobile'])
except:
# 未注册过,注册为新用户
return attrs
else:
# 注册过 查询用户进行绑定
# 判断密码
if not user.check_password(attrs['password']):
raise serializers.ValidationError('密码错误')
attrs['user'] = user
return attrs
def create(self, validated_data):
"""
保存用户
:param self:
:param validated_data:
:return:
"""
# 判断用户
user = validated_data.get('user', None)
if user is None:
# 创建用户
user = User.objects.create_user(username=validated_data['mobile'],
password=validated_data['password'],
mobile=validated_data['mobile'])
# 绑定操作
OAuthUser.objects.create(user=user, openid=validated_data["openid"])
# user_id=user.id
# 生成加密后的token数据
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user) # 生成载荷部分
token = jwt_encode_handler(payload) # 生成token
# user添加token属性
user.token = token
user.user_id = user.id
return user
二、微博登录:
1.前期准备:
新浪微博登录,亦即我们所说的第三方登录,是指用户可以不在本项目中输入密码,而直接通过第三方的验证,成功登录本项目。
若想实现微博登录,需要成为微博开发者,审核通过才可实现。注册方法可参考链接
https://open.weibo.com/connect
成为微博互联开发者后,还需创建应用,即获取本项目对应与微博互联的应用ID,创建应用的方法参考链接
https://open.weibo.com/authentication
微博登录开发文档连接:
https://open.weibo.com/wiki/%E6%8E%88%E6%9D%83%E6%9C%BA%E5%88%B6%E8%AF%B4%E6%98%8E
微博的ai接口:
https://open.weibo.com/wiki/Connect/login
新浪微博错误码列表:
http://blog.unvs.cn/archives/sina-api-error-code.html
2.创建模型类:
参考QQ登录时创建的数据模型
3.微博登录认证–oauth2.0:
OAuth2.0较1.0相比,整个授权验证流程更简单更安全,也是未来最主要的用户身份验证和授权方式。
关于OAuth2.0协议的授权流程可以参考下面的流程图,其中Client指第三方应用,Resource Owner指用户,Authorization Server是我们的授权服务器,Resource Server是API服务器。
3.1. 引导需要授权的用户到如下地址:
URL:
https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI
3.2 如果用户同意授权,页面跳转至 YOUR_REGISTERED_REDIRECT_URI/?code=CODE
3.3换取Access Token
URL
https://api.weibo.com/oauth2/access_token?client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=authorization_code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI&code=CODE
其中client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET可以使用basic方式加入header中,返回值
JSON
{
"access_token": "SlAV32hkKG",
"remind_in": 3600,
"expires_in": 3600
}
3.4 使用获得的Access Token调用API
3.微博登录SDK使用:
- 由微博官方给出的文档,自定义微博登录SDK:
class OAuthWeibo(object):
""" 微博认证辅助工具类 """
def __init__(self,client_id = None, client_secret = None, redirect_uri = None, state=None):
self.client_id = client_id or settings.WEIBO_CLIENT_ID
self.redirect_uri = redirect_uri or settings.WEIBO_REDIRECT_URI
self.client_secret=client_secret or settings.WEIBO_CLIENT_SECRET
self.state = state or settings.WEIBO_STATE
def get_weibo_url(self):
"""
获取微博的验证页面链接
:return:
"""
data_dict = {
'client_id': self.client_id,
'redirect_uri': self.redirect_uri,
'state':self.state
}
# print(data_dict)
# 构造微博登录链接
weibo_url = 'https://api.weibo.com/oauth2/authorize?' + urlencode(data_dict)
return weibo_url
def get_access_token(self, code):
"""
获取微博的accesstoken值
https://api.weibo.com/oauth2/access_token
?client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&grant_type=authorization_code
&redirect_uri=YOUR_REGISTERED_REDIRECT_URI
&code=CODE
:param code:
:return:
"""
# 构造参数
data_dict = {
"client_id":self.client_id,
"client_secret":self.client_secret,
"grant_type":"authorization_code",
"redirect_uri":self.redirect_uri,
"code":code
}
# 发送请求
url = "https://api.weibo.com/oauth2/access_token"
try:
response = requests.post(url=url,data=data_dict)
# 提取数据
# {
# "access_token": "SlAV32hkKG",
# "remind_in": 3600,
# "expires_in": 3600
# }
data = response.text
data_dict = json.loads(data)
access_token = data_dict["access_token"]
print(data_dict)
except:
raise Exception('微博请求失败')
return access_token
4.返回微博登录网址的视图:
4.1 后端接口设计:
请求方式: GET oauth/weibo/authorization/?next=/
请求参数: 查询字符串
参数名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
next | str | 否 | 用户微博登录成功后进入美多商城的哪个网址 |
返回数据: JSON
|返回值| 类型| 是否必须| 说明|
|login_url| str |是 |微博登录网址|
{
"login_url":"https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI"
}
4.2 准备匹配置信息:
在配置文件中添加关于微博登录的应用开发信息:
# settins.dev.py
# 微博登录的参数
WEIBO_CLIENT_ID = '33******85'
WEIBO_CLIENT_SECRET = '74**************************76'
WEIBO_REDIRECT_URI = 'http://www.meiduo.site:8080/sina_callback.html'
4.3 后端逻辑实现:
在oauth/views.py中实现后端逻辑:
class WeiboAuthURLLView(APIView):
"""定义微博第三方登录的视图类"""
def get(self, request):
"""
获取微博登录的链接
oauth/weibo/authorization/?next=/
:param request:
:return:
"""
# 1.通过查询字符串
next = request.query_params.get('state')
if not next:
next = "/"
# 获取微博登录网页
oauth = OAuthWeibo(client_id=settings.WEIBO_CLIENT_ID,
client_secret=settings.WEIBO_CLIENT_SECRET,
redirect_uri=settings.WEIBO_REDIRECT_URI,
state=next)
login_url = oauth.get_weibo_url()
return Response({"login_url": login_url})
4.3 前端:
修改login.js, 在methods中增加weibo_login方法:
// 微博登录
weibo_login: function(){
var next = this.get_query_string('next') || '/';
axios.get(this.host + '/oauth/weibo/authorization/?next=' + next, {
responseType: 'json',
withCredentials: true
})
.then(response => {
location.href = response.data.login_url;
})
.catch(error => {
console.log(error.response.data);
})
}
5.获取微博用户的token
在微博将用户重定向到此网页的时候,重定向的网址会携带微博提供的code参数,用于获取用户信息使用,我们需要将这个code参数发送给后端,在后端中使用code参数向微博请求用户的身份信息,并查询与该微博用户绑定的用户。
5.1 后端接口设计:
请求方式:GET oauth/sina/user/?code=0e*******a6a
**请求参数:**查询字符串参数
|参数| 类型| 是否必传| 说明|
|code| str| 是 |微博返回的授权凭证code|
**返回数据:**JSON
返回值 | 类型 | 是否必须 | 说明 |
---|---|---|---|
access_token | str | 否 | 用户是第一次使用微博登录时返回,其中包含weibotoken,用于绑定身份使用,注意这个是我们自己生成的 |
token | str | 否 | 用户不是第一次使用微博登录时返回,登录成功的JWT token |
username | str | 否 | 用户不是第一次使用微博登录时返回,用户名 |
user_id int | 否 | 用户不是第一次使用weibo登录时返回,用户id |
class WeiboOauthView(APIView):
"""验证微博登录"""
def get(self, request):
"""
第三方登录检查
oauth/sina/user/
?code=0e67548e9e075577630cc983ff79fa6a
:param request:
:return:
"""
# 1.获取code值
code = request.query_params.get("code")
# 2.检查参数
if not code:
return Response({'errors': '缺少code值'}, status=400)
# 3.获取token值
next = "/"
# 获取微博登录网页
weiboauth = OAuthWeibo(client_id=settings.WEIBO_CLIENT_ID,
client_secret=settings.WEIBO_CLIENT_SECRET,
redirect_uri=settings.WEIBO_REDIRECT_URI,
state=next)
weibotoken = weiboauth.get_access_token(code=code)
print(weibotoken)
# 5.判断是否绑定过美多账号
try:
weibo_user = OAuthUser.objects.get(weibotoken=weibotoken)
except:
# 6.未绑定,进入绑定页面,完成绑定
tjs = TJS(settings.SECRET_KEY, 300)
weibotoken = tjs.dumps({'weibotoken': weibotoken}).decode()
return Response({'access_token': weibotoken})
else:
# 7.绑定过,则登录成功
# 生成jwt-token值
user = weibo_user.user
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user) # 生成载荷部分
token = jwt_encode_handler(payload) # 生成token
response = Response(
{
'token': token,
'username': user.username,
'user_id': user.id
}
)
return response
6.weiboTkoen绑定美多商城用户:
如果用户是首次使用微博登录,则需要绑定用户,页面如下:
业务逻辑:
用户需要填写手机号、密码、图片验证码、短信验证码
如果用户未在美多商城注册过,则会将手机号作为用户名为用户创建一个美多账户,并绑定用户
如果用户已在美多商城注册过,则检验密码后直接绑定用户
6.1.后端接口设计
请求方式: POST /oauth/sina/user/
请求方式: JSON
参数 | 类型 | 是否必须 | 说明 |
---|---|---|---|
mobile | str | 是 | 手机号 |
password | str | 是 | 密码 |
sms_code | str | 是 | 短信验证码 |
access_token | str | 是 | 凭据 (包含微博token) |
返回数据: JSON
返回值 | 类型 | 是否必须 | 说明 |
---|---|---|---|
token | str | 是 JWT token | |
id | int | 是 | 用户id |
username | str | 是 | 用户名 |
6.2 后端逻辑实现:
# 在oauth/views.py修改WeiboOauthView视图
class WeiboOauthView(APIView):
"""验证微博登录"""
def get(self, request):
...
def post(self,request):
"""
微博用户未绑定,绑定微博用户
:return:
"""
# 1. 获取前端数据
data = request.data
# 2.调用序列化起验证数据
ser = WeiboOauthSerializers(data=data)
ser.is_valid()
print(ser.errors)
# 保存绑定数据
ser.save()
return Response(ser.data)
# oauth/serializers.py
class WeiboOauthSerializers(serializers.ModelSerializer):
"""微博验证序列化器"""
# 指名模型类中没有的字段
mobile = serializers.CharField(max_length=11)
sms_code = serializers.CharField(max_length=6, min_length=6, write_only=True)
access_token = serializers.CharField(write_only=True) # 反序列化输入
token = serializers.CharField(read_only=True)
user_id = serializers.IntegerField(read_only=True) # 序列化输出
class Meta:
model = User
fields = ('password', 'mobile', 'username', 'sms_code', 'token', 'access_token', 'user_id')
extra_kwargs = {
'username': {
'read_only': True
},
'password': {
'write_only': True
}
}
def validated_mobile(self, value):
"""
验证手机号
:param value:
:return:
"""
if not re.match(r"1[3-9]\d{9}$", value):
raise serializers.ValidationError("手机号格式错误")
return value
def validate(self, attrs):
"""
验证access_token
:param attrs:
:return:
"""
tjs = TJS(settings.SECRET_KEY, 300)
try:
data = tjs.loads(attrs["access_token"]) # 解析token
except:
raise serializers.ValidationError("无效的token")
# 获取weibotoken
weibotoken = data.get("weibotoken")
# attrs中添加weibotoken
attrs["weibotoken"] = weibotoken
# 验证短信验证码:
rel_sms_code = SMSCodeView.checkSMSCode(attrs["mobile"])
if not rel_sms_code:
raise serializers.ValidationError('短信验证码失效')
# 3、比对用户输入的短信和redis中真实短信
if attrs['sms_code'] != rel_sms_code:
raise serializers.ValidationError('短信验证不一致')
# 验证手机号是否被注册过
try:
user = User.objects.get(mobile=attrs['mobile'])
except:
# 未注册过,注册为新用户
return attrs
else:
# 注册过 查询用户进行绑定
# 判断密码
if not user.check_password(attrs['password']):
raise serializers.ValidationError('密码错误')
attrs['user'] = user
return attrs
def create(self, validated_data):
"""
保存用户
:param self:
:param validated_data:
:return:
"""
# 判断用户
user = validated_data.get('user', None)
if user is None:
# 创建用户
user = User.objects.create_user(username=validated_data['mobile'],
password=validated_data['password'],
mobile=validated_data['mobile'])
# 绑定操作
OAuthUser.objects.create(user=user, weibotoken=validated_data["weibotoken"])
# user_id=user.id
# 生成加密后的token数据
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user) # 生成载荷部分
token = jwt_encode_handler(payload) # 生成token
# user添加token属性
user.token = token
user.user_id = user.id
return user