Django中使用QQ登录
Django中使用QQ登录
1.返回QQ登录网址的视图
(1)后端接口设计:
请求方式: GET /oauth/qq/authorization/?next=xxx
请求参数: 查询字符串
参数名 | 类型 | 是否必须 | 说明 |
---|---|---|---|
next | str | 否 | 用户QQ登录成功后进入网站的哪个具体网址 |
返回数据: JSON
{
"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"
}
返回值 | 类型 | 是否必须 | 说明 |
---|---|---|---|
login_url | str | 是 | qq登录网址 |
(2)准备配置信息:
在配置文件中添加关于QQ登录的应用开发信息
# QQ登录参数,填写自己的应用信息
QQ_CLIENT_ID = '101******'
QQ_CLIENT_SECRET = 'c6ce949e04e**********'
QQ_REDIRECT_URI = 'http://xxx.com/xxx.html'
(3)后端逻辑实现:
在视图中实现后端逻辑
# url(r'^qq/authorization/$', views.QQAuthURLView.as_view()),
class QQAuthURLView(APIView):
"""提供QQ登录页面网址
https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=xxx&redirect_uri=xxx&state=xxx
"""
def get(self, request):
# next表示从哪个页面进入到的登录页面,将来登录成功后,就自动回到那个页面
next = request.query_params.get('next')
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})
(4)前端
编写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);
})
}
2.准备回调页
用户在QQ登录成功后,QQ会将用户重定向回我们配置的回调callback网址:
http://xxx.com/xxx.html
我们在前端项目目录中新建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 xxx商业股份有限公司 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: {
// data部分的数据不重要,不需要理解是干嘛的,重要的是前端将code发给后端
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;
},
});
3.获取QQ用户OpenID
在QQ将用户重定向到回调页的时候,重定向的网址会携带QQ提供的code参数,用于获取用户信息使用,我们需要将这个code参数发送给后端,在后端中使用code参数向QQ请求用户的身份信息,并查询与该QQ用户绑定的用户,如果绑定了,也就是用户不是第一次使用QQ登录,后端就返回token,进行状态保持,如果没有绑定,后端会把openid进行加密处理后返回前端,让前端保存,当用户在前端填写完表单绑定用户时,前端把用户信息和加密的openid一起发给后端,后端解密openid,把用户信息和openid分别存进对应的表中,完成绑定并且返回token完成状态保持。
(1)后端接口设计
请求方式 : GET /oauth/qq/user/?code=xxx
请求参数: 查询字符串参数
参数 | 类型 | 是否必传 | 说明 |
---|---|---|---|
code | str | 是 | qq返回的授权凭证code |
返回数据: JSON
{
"access_token": xxxx,
}
或
{
"token": "xxx",
"username": "python",
"user_id": 1
}
返回值 | 类型 | 是否必须 | 说明 |
---|---|---|---|
access_token | str | 否 | 用户是第一次使用QQ登录时返回,其中包含openid,用于绑定身份使用,注意这个是我们自己生成的 |
token | str | 否 | 用户不是第一次使用QQ登录时返回,登录成功的JWT token |
username | str | 否 | 用户不是第一次使用QQ登录时返回,用户名 |
user_id | int | 否 | 用户不是第一次使用QQ登录时返回,用户id |
(2)后端逻辑实现
-
创建模型类
创建模型类基类,用于增加数据新建时间和更新时间。
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/models.py中定义QQ身份(openid)与用户模型类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) class Meta: db_table = 'tb_oauth_qq' verbose_name = 'QQ登录用户数据' verbose_name_plural = verbose_name
进行数据库迁移
python manage.py makemigrations python manage.py migrate
-
在views.py中实现后端逻辑
# url(r'^qq/user/$', views.QQAuthUserView.as_view()),
class QQAuthUserView(GenericAPIView):
"""用户扫码登录的回调处理"""
# 指定序列化器
serializer_class = serializers.QQAuthUserSerializer
def get(self, request):
# 提取code请求参数
code = request.query_params.get('code')
if not code:
return Response({'message':'缺少code'}, status=status.HTTP_400_BAD_REQUEST)
# 创建工具对象
oauth = OAuthQQ(client_id=settings.QQ_CLIENT_ID, client_secret=settings.QQ_CLIENT_SECRET,
redirect_uri=settings.QQ_REDIRECT_URI)
try:
# 使用code向QQ服务器请求access_token
access_token = oauth.get_access_token(code)
# 使用access_token向QQ服务器请求openid
openid = oauth.get_open_id(access_token)
except Exception:
return Response({'message': 'QQ服务异常'}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
(3)根据OpenID查询用户
- 如果能够查询到美多商城用户,就直接生成状态保持信息,登录到美多商城
- 如果不能查询到美多商城用户,就直接将OpenID序列化并返回给前端,用于后续的绑定美多商城用户操作
# url(r'^qq/user/$', views.QQAuthUserView.as_view()),
class QQAuthUserView(GenericAPIView):
"""用户扫码登录的回调处理"""
# 指定序列化器
serializer_class = serializers.QQAuthUserSerializer
def get(self, request):
# 提取code请求参数
code = request.query_params.get('code')
if not code:
return Response({'message':'缺少code'}, status=status.HTTP_400_BAD_REQUEST)
# 创建工具对象
oauth = OAuthQQ(client_id=settings.QQ_CLIENT_ID, client_secret=settings.QQ_CLIENT_SECRET,
redirect_uri=settings.QQ_REDIRECT_URI)
try:
# 使用code向QQ服务器请求access_token
access_token = oauth.get_access_token(code)
# 使用access_token向QQ服务器请求openid
openid = oauth.get_open_id(access_token)
except Exception:
return Response({'message': 'QQ服务异常'}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
# 使用openid查询该QQ用户是否在美多商城中绑定过用户
try:
oauth_user = OAuthQQUser.objects.get(openid=openid)
except OAuthQQUser.DoesNotExist:
# 如果openid没绑定美多商城用户,创建用户并绑定到openid
# 为了能够在后续的绑定用户操作中前端可以使用openid,在这里将openid签名后响应给前端
access_token_openid = generate_save_user_token(openid) # generate_save_user_token方法为自己编写的加密token的方法,下面会介绍
return Response({'access_token':access_token_openid})
else:
# 如果openid已绑定美多商城用户,直接生成JWT token,并返回
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
# 获取oauth_user关联的user
user = oauth_user.user
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
response = Response({
'token': token,
'user_id': user.id,
'username': user.username
})
return response
- 准备序列化OpenID的工具方法
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from django.conf import settings
def generate_save_user_token(openid):
"""
生成保存用户数据的token
:param openid: 用户的openid
:return: token
"""
serializer = Serializer(settings.SECRET_KEY, expires_in=constants.SAVE_QQ_USER_TOKEN_EXPIRES)
data = {'openid': openid}
token = serializer.dumps(data)
return token.decode()
补充:使用itsdangerous的使用
-
itsdangerous模块的参考资料连接http://itsdangerous.readthedocs.io/en/latest/
-
安装:
pip install itsdangerous
== 1.1.0 -
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
4.OpenID绑定美多商城用户
如果用户是首次使用QQ登录,则需要绑定用户。
业务逻辑:
- 用户需要填写手机号、密码、图片验证码、短信验证码
- 如果用户未在美多商城注册过,则会将手机号作为用户名为用户创建一个账户,并绑定用户
- 如果用户已在美多商城注册过,则检验密码后直接绑定用户
(1)后端接口设计
请求方式: 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 | 是 | 用户名 |
(2)后端逻辑实现
- 修改QQAuthUserView视图,添加post方法:
# url(r'^qq/user/$', views.QQAuthUserView.as_view()),
class QQAuthUserView(GenericAPIView):
"""用户扫码登录的回调处理"""
# 指定序列化器
serializer_class = serializers.QQAuthUserSerializer
def get(self, request):
......
def post(self, request):
"""openid绑定到用户"""
# 获取序列化器对象
serializer = self.get_serializer(data=request.data)
# 开启校验
serializer.is_valid(raise_exception=True)
# 保存校验结果,并接收
user = serializer.save()
# 生成JWT 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)
response = Response({
'token': token,
'user_id': user.id,
'username': user.username
})
return response
- 新建serializers.py文件,编写序列化器:
class QQAuthUserSerializer(serializers.Serializer):
"""
QQ登录创建用户序列化器
"""
access_token = serializers.CharField(label='操作凭证')
mobile = serializers.RegexField(label='手机号', regex=r'^1[3-9]\d{9}$')
password = serializers.CharField(label='密码', max_length=20, min_length=8)
sms_code = serializers.CharField(label='短信验证码')
def validate(self, data):
# 检验access_token
access_token = data['access_token']
# 获取身份凭证
openid = check_save_user_token(access_token) # check_save_user_token 解密openid
if not openid:
raise serializers.ValidationError('无效的access_token')
# 将openid放在校验字典中,后面会使用
data['openid'] = openid
# 检验短信验证码
mobile = data['mobile']
sms_code = data['sms_code']
redis_conn = get_redis_connection('verify_codes')
real_sms_code = redis_conn.get('sms_%s' % mobile)
if real_sms_code.decode() != sms_code:
raise serializers.ValidationError('短信验证码错误')
# 如果用户存在,检查用户密码
try:
user = User.objects.get(mobile=mobile)
except User.DoesNotExist:
pass
else:
password = data['password']
if not user.check_password(password):
raise serializers.ValidationError('密码错误')
# 将认证后的user放进校验字典中,后续会使用
data['user'] = user
return data
def create(self, validated_data):
# 获取校验的用户
user = validated_data.get('user')
if not user:
# 用户不存在,新建用户
user = User.objects.create_user(
username=validated_data['mobile'],
password=validated_data['password'],
mobile=validated_data['mobile'],
)
# 将用户绑定openid
OAuthQQUser.objects.create(
openid=validated_data['openid'],
user=user
)
# 返回用户数据
return user
- 准备序列化OpenID的工具方法
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from django.conf import settings
def check_save_user_token(access_token):
"""
检验保存用户数据的token
:param token: token
:return: openid or None
"""
serializer = Serializer(settings.SECRET_KEY, expires_in=constants.SAVE_QQ_USER_TOKEN_EXPIRES)
try:
data = serializer.loads(access_token)
except BadData:
return None
else:
return data.get('openid')
(3)前端
// 保存
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);
}
})
}
}