Django项目QQ登录后端接口实现
QQ登录,亦即我们所说的第三方登录,是指用户可以不在本项目中输入密码,而直接通过第三方的验证,成功登录本项目。
1.准备工作的步骤:
QQ登录网站开发流程准备工作:
1.打开下列腾讯开放平台网址,注册申请appid和appkey。
http://wiki.connect.qq.com/准备工作_oauth2-0
2.下载“QQ登录”按钮图片,并将按钮放置在页面合适的位置,并为“QQ登录”按钮添加前台代码。详见:
http://wiki.connect.qq.com/放置qq登录按钮_oauth2-0
3.理解QQ登录的流程
2.正式开始QQ登录开发流程:
- 创建模型类,用于存放网址用户和用户的QQ获取到的openid的绑定关系表。
from django.db import models
class OAuthQQUser(models.Model):
"""
QQ登录用户数据
"""
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")
user = models.ForeignKey('users.User', on_delete=models.CASCADE, verbose_name='用户') # 此处的users为项目的一个子应用,User为项目的用户模型类
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
执行项目的数据库迁移,在项目启动文件manage.py的同级目录下进行数据库迁移操作。
python manage.py makemigrations
python manage.py migrate`
-
urllib使用说明
在后端接口中,我们需要向QQ服务器发送请求,查询用户的QQ信息,Python提供了标准模块urllib可以帮助我们发送http请求。 -
urllib.parse.urlencode(query)
将query字典转换为url路径中的查询字符串
- urllib.parse.parse_qs(qs)
将qs查询字符串格式数据转换为python的字典
- urllib.request.urlopen(url, data=None)
发送http请求,如果data为None,发送GET请求,如果data不为None,发送POST请求
返回response响应对象,可以通过read()读取响应体数据,需要注意读取出的响应体数据为bytes类型
2.1第一步:
客户端点击QQ登录按钮时向后端发起请求,服务器返回给客户端一个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_ur
类型:str
是否必须:是
说明:qq登录网址
- 1.1.在配置文件settings中添加关于QQ登录的应用开发信息
QQ_CLIENT_ID = '101474184' # appid
QQ_CLIENT_SECRET = 'c6ce949e04e12ecc909ae6a8b09b637c' # appkey
QQ_REDIRECT_URI = 'http://www.meiduo.site:8080/oauth_callback.html' # 成功授权后的回调地址,必须是注册appid时填写的主域名下的地址,建议设置为网站首页或网站的用户中心。
QQ_STATE = '/' # 用户登陆成功以后需要跳转的当前开发的项目的某个指定的地址,这里默认首页
- 1.2创建一个QQ登录的子应用,终端cd到manage.py目录下,例如oauth应用。
python manag.py startapp oauth
- 1.3 settings文件中注册子应用
INSTALLED_APPS = (
...
'oauth.apps.OauthConfig',
...
)
- 1.4 新建
oauth/utils.py
文件,创建QQ登录辅助工具类
from urllib.parse import urlencode, parse_qs
from urllib.request import urlopen
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, BadData
from django.conf import settings
import json
import logging
from . import constants
logger = logging.getLogger('django') # django先前配置了相关日志名称和对应的等级
class OAuthQQ(object):
"""
QQ认证辅助工具类
"""
def __init__(self, client_id=None, client_secret=None, redirect_uri=None, state=None):
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_login_url(self):
"""
获取qq登录的网址
:return: url网址
"""
url = 'https://graph.qq.com/oauth2.0/authorize?' # qq开发对外提供的地址加问号用于拼接查询参数
params = {
'response_type': 'code',
'client_id': self.client_id,
'redirect_uri': self.redirect_uri,
'state': self.state,
'scope': 'get_user_info',
}
url += urlencode(params)
return url
- 1.5 在oauth/views.py中实现视图
# url(r'^qq/authorization/$', views.QQAuthURLView.as_view()),
class QQAuthURLView(APIView):
"""
获取QQ登录的url
"""
def get(self, request):
"""
提供用于qq登录的url
"""
# 获取查询参数next即登录成功后跳转的网址
next = request.query_params.get('next')
# 传入下一跳地址,创建OauthQQ的实例对象
oauth = OAuthQQ(state=next)
# 获取QQ登录网址并json格式返回
login_url = oauth.get_qq_login_url()
return Response({'login_url': login_url})
2.2 第二步:QQ登录回调处理
用户在QQ登录成功后,QQ会将用户重定向回我们配置的回调callback网址,例如,我们申请QQ登录开发资质时配置的回调地址为:
http://www.XXXX.site:8080/oauth_callback.html
在QQ将用户重定向到此网页的时候,重定向的网址会携带QQ提供的code参数,用于获取用户信息使用,我们需要将这个code参数发送给后端,在后端中使用code参数向QQ请求用户的身份信息,并查询与该QQ用户绑定的用户。
2.2.1后端接口设计
请求方式 :
GET /oauth/qq/user/?code=xxx
请求参数: 查询字符串参数
- 参数:code
- 类型:str
- 是否必传:是
- 说明:qq返回的授权凭证code
- 返回数据: JSON
{
"access_token": xxxx,
}
或
{
"token": "xxx",
"username": "python",
"user_id": 1
}
返回值
- 在OAuthQQ辅助类中添加方法:
import json
import re
from django.conf import settings
import urllib.parse
import urllib.request
import logging
from itsdangerous import BadData
from itsdangerous import TimedJSONWebSignatureSerializer as TJWSSerializer
from oauth.constants import BIND_USER_ACCESS_TOKEN_EXPIRES
from oauth.exceptions import OaAuthQQAPIError
logger = logging.getLogger("django")
class OauthQQ(object):
"""
QQ认证辅助工具类
"""
def __init__(self, client_id=None, client_secret=None, redirect_uri=None, state=None):
self.client_id = client_id if client_id else settings.QQ_CLIENT_ID
self.redirect_uri = redirect_uri if redirect_uri else settings.QQ_REDIRECT_URI
# self.state = state if state else settings.QQ_STATE
self.state = state or settings.QQ_STATE # 两种写法
self.client_secret = client_secret if client_secret else settings.QQ_CLIENT_SECRET
def get_login_url(self):
"""获取登录地址"""
url = "https://graph.qq.com/oauth2.0/authorize?"
params = {
'response_type': 'code',
'client_id': self.client_id,
'redirect_uri': self.redirect_uri,
'state': self.state
}
url += urllib.parse.urlencode(params)
return url
def get_access_token(self, code):
"""获取access_token"""
url = 'https://graph.qq.com/oauth2.0/token?'
params = {
"grant_type": "authorization_code",
"client_id": self.client_id,
"client_secret": self.client_secret,
"code": code,
"redirect_uri": self.redirect_uri
}
url += urllib.parse.urlencode(params)
# 发送请求
try:
resp = urllib.request.urlopen(url)
# 读取响应体数据
resp_data = resp.read()
# 将得到的bytes类型转为str
resp_str = resp_data.decode()
# access_token=FE04****CCE2&expires_in=7776000&refresh_token=88E4*****BE14
# 解析access_token
resp_dict = urllib.parse.parse_qs(resp_str)
except Exception as e:
logger.error("获取access_token异常:%s" % e)
raise OaAuthQQAPIError
else:
access_token = resp_dict.get('access_token')
# print(access_token)
return access_token[0]
def get_openid(self, access_token):
"""根据access_token获取qq用户的openid"""
url = 'https://graph.qq.com/oauth2.0/me?access_token=' + access_token
try:
# 发送请求
resp = urllib.request.urlopen(url)
# 读取响应数据
resp_data = resp.read()
resp_str = resp_data.decode()
# callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
# 解析openid
resp_dict_str = re.match(r'.*(\{.*\}).*', resp_str).group(1) # 匹配出字典
resp_dict = json.loads(resp_dict_str) # str转换为字典格式
except Exception as e:
logger.error("获取openid异常:%s" % e)
raise OaAuthQQAPIError
else:
openid = resp_dict.get("openid")
return openid
@staticmethod
def generate_user_bind_access_token(openid):
"""
生成用户绑定令牌token
:param openid: 从qq获取到的openid
:return:
"""
# serializer = Serializer(秘钥, 有效期秒)
serializer = TJWSSerializer(settings.SECRET_KEY, BIND_USER_ACCESS_TOKEN_EXPIRES)
# serializer.dumps(数据), 返回bytes类型
token = serializer.dumps({"openid": openid})
token = token.decode()
return token
在oauth/views.py中实现视图
from rest_framework import status
from rest_framework.generics import CreateAPIView
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_jwt.settings import api_settings
from oauth.exceptions import OaAuthQQAPIError
from oauth.models import OauthQQUser
from oauth.serializers import OAuthQQUserSerializer
from .utils import OauthQQ
# url(r'^qq/authorization/$', views.QQAuthURLView.as_view()),
class QQAuthURLView(APIView):
"""
获取QQ登录的url ?next=xxxxxx
"""
def get(self, request):
# 获取next参数
next = request.query_params.get("next")
# 拼接qq登录的url的路径
oauth_qq = OauthQQ(state=next)
login_url = oauth_qq.get_login_url()
# 返回url
return Response({'login_url': login_url})
class QQAuthUserView(CreateAPIView):
"""
通过QQ按钮登录的视图
"""
serializer_class = OAuthQQUserSerializer
def get(self, request):
# 获取code
code = request.query_params.get('code')
if not code:
return Response({"meaasge": "缺少code"}, status=status.HTTP_400_BAD_REQUEST)
oauth_qq = OauthQQ()
try:
# 根据code,获取access_token
access_token = oauth_qq.get_access_token(code)
# 根据access_token 获取openid
openid = oauth_qq.get_openid(access_token)
except OaAuthQQAPIError:
return Response({"message": "访问QQ接口异常"}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
# 根据openid查询数据库,判断用户是否存在
try:
oauth_qq_user = OauthQQUser.objects.get(openid=openid)
except OauthQQUser.DoesNotExist:
# 如果数据不存在,处理openid(加密)并返回
new_access_token = oauth_qq.generate_user_bind_access_token(openid)
return Response({"access_token": new_access_token})
else:
# 如果数据存在,表示用户已经绑定过身份,签发JWT_token
# 签发jwt token
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
user = oauth_qq_user.user
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return Response({
"username": user.username,
"user_id": user.id,
"token": token
})
2.3绑定用户身份接口
如果用户是首次使用QQ登录,则需要绑定用户
业务逻辑:
- 用户需要填写手机号、密码、图片验证码、短信验证码
- 如果用户未注册过,则会将手机号作为用户名为用户创建一个账户,并绑定用户
- 如果用户已在注册过,则检验密码后直接绑定用户
- 绑定QQ身份的处理流程
2.3.1后端接口设计
在OAuthQQ辅助类中增加
import json
import re
from django.conf import settings
import urllib.parse
import urllib.request
import logging
from itsdangerous import BadData
from itsdangerous import TimedJSONWebSignatureSerializer as TJWSSerializer
from oauth.constants import BIND_USER_ACCESS_TOKEN_EXPIRES
from oauth.exceptions import OaAuthQQAPIError
logger = logging.getLogger("django")
class OauthQQ(object):
"""
QQ认证辅助工具类
"""
def __init__(self, client_id=None, client_secret=None, redirect_uri=None, state=None):
self.client_id = client_id if client_id else settings.QQ_CLIENT_ID
self.redirect_uri = redirect_uri if redirect_uri else settings.QQ_REDIRECT_URI
# self.state = state if state else settings.QQ_STATE
self.state = state or settings.QQ_STATE # 两种写法
self.client_secret = client_secret if client_secret else settings.QQ_CLIENT_SECRET
def get_login_url(self):
"""获取登录地址"""
url = "https://graph.qq.com/oauth2.0/authorize?"
params = {
'response_type': 'code',
'client_id': self.client_id,
'redirect_uri': self.redirect_uri,
'state': self.state
}
url += urllib.parse.urlencode(params)
return url
def get_access_token(self, code):
"""获取access_token"""
url = 'https://graph.qq.com/oauth2.0/token?'
params = {
"grant_type": "authorization_code",
"client_id": self.client_id,
"client_secret": self.client_secret,
"code": code,
"redirect_uri": self.redirect_uri
}
url += urllib.parse.urlencode(params)
# 发送请求
try:
resp = urllib.request.urlopen(url)
# 读取响应体数据
resp_data = resp.read()
# 将得到的bytes类型转为str
resp_str = resp_data.decode()
# access_token=FE04****CCE2&expires_in=7776000&refresh_token=88E4*****BE14
# 解析access_token
resp_dict = urllib.parse.parse_qs(resp_str)
except Exception as e:
logger.error("获取access_token异常:%s" % e)
raise OaAuthQQAPIError
else:
access_token = resp_dict.get('access_token')
# print(access_token)
return access_token[0]
def get_openid(self, access_token):
"""根据access_token获取qq用户的openid"""
url = 'https://graph.qq.com/oauth2.0/me?access_token=' + access_token
try:
# 发送请求
resp = urllib.request.urlopen(url)
# 读取响应数据
resp_data = resp.read()
resp_str = resp_data.decode()
# callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
# 解析openid
resp_dict_str = re.match(r'.*(\{.*\}).*', resp_str).group(1) # 匹配出字典
resp_dict = json.loads(resp_dict_str) # str转换为字典格式
except Exception as e:
logger.error("获取openid异常:%s" % e)
raise OaAuthQQAPIError
else:
openid = resp_dict.get("openid")
return openid
@staticmethod
def generate_user_bind_access_token(openid):
"""
生成用户绑定令牌token
:param openid: 从qq获取到的openid
:return:
"""
# serializer = Serializer(秘钥, 有效期秒)
serializer = TJWSSerializer(settings.SECRET_KEY, BIND_USER_ACCESS_TOKEN_EXPIRES)
# serializer.dumps(数据), 返回bytes类型
token = serializer.dumps({"openid": openid})
token = token.decode()
return token
@staticmethod
def check_user_bind_access_token(access_token):
"""
解析用户令牌token,从中获取QQ用户的openid
:param access_token:
:return:
"""
# serializer = Serializer(秘钥, 有效期秒)
serializer = TJWSSerializer(settings.SECRET_KEY, BIND_USER_ACCESS_TOKEN_EXPIRES)
# serializer.load(数据)
try:
data = serializer.loads(access_token)
except BadData:
return None
return data.get("openid")
新建oauth/serializers.py文件,
from django_redis import get_redis_connection
from rest_framework import serializers
from rest_framework_jwt.settings import api_settings
from oauth.models import OauthQQUser
from users.models import User
from .utils import OauthQQ
class OAuthQQUserSerializer(serializers.ModelSerializer):
sms_code = serializers.CharField(label='短信验证码', write_only=True)
access_token = serializers.CharField(label='操作凭证', write_only=True)
token = serializers.CharField(read_only=True)
mobile = serializers.RegexField(label="手机号", regex=r'^1[3-9]\d{9}$')
class Meta:
model = User
fields = ['mobile', 'password', 'sms_code', 'access_token', 'username', 'id', 'token']
extra_kwargs = {
"username": {
"read_only": True # 用户名在QQ登录时是手机号,加上条件使反序列化时不尽进行校验
},
"password": {
'write_only': True,
'min_length': 8,
'max_length': 20,
'error_messages': {
'min_length': '仅允许8-20个字符的密码',
'max_length': '仅允许8-20个字符的密码',
}
}
}
def validate(self, attrs):
# 检验access_token
access_token = attrs["access_token"]
# 解析出openid
openid = OauthQQ.check_user_bind_access_token(access_token)
if not openid:
raise serializers.ValidationError('无效的access_token')
# 添加数据
attrs['openid'] = openid
# 检验短信验证码
mobile = attrs['mobile']
sms_code = attrs["sms_code"]
redis_conn = get_redis_connection('verify_codes')
real_sms_code = redis_conn.get('sms_%s' % mobile) # type:bytes
if real_sms_code.decode() != sms_code:
raise serializers.ValidationError('短信验证码错误')
# 判断用户是否存在
try:
user = User.objects.get(mobile=mobile)
except User.DoesNotExist:
pass
else:
# 用户存在,校验密码
password = attrs["password"]
if not user.check_password(password):
raise serializers.ValidationError('密码错误')
# 把用户对象添加到参数字典
attrs['user'] = user
return attrs
def create(self, validated_data):
openid = validated_data['openid']
user = validated_data.get('user')
mobile = validated_data['mobile']
password = validated_data['password']
if not user:
# 如果用户不存在,则创建User用户,再创建OauthQQUser用户
user = User.objects.create_user(username=mobile, mobile=mobile, password=password)
# 如果用户存在,绑定用户,创建OauthQQUser用户
OauthQQUser.objects.create(user=user, openid=openid)
# 签发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)
# 给user添加token属性
user.token = token
return user
在oauth/views.py修改QQAuthUserView 视图
from rest_framework import status
from rest_framework.generics import CreateAPIView
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_jwt.settings import api_settings
from oauth.exceptions import OaAuthQQAPIError
from oauth.models import OauthQQUser
from oauth.serializers import OAuthQQUserSerializer
from .utils import OauthQQ
# url(r'^qq/authorization/$', views.QQAuthURLView.as_view()),
class QQAuthURLView(APIView):
"""
获取QQ登录的url ?next=xxxxxx
"""
def get(self, request):
# 获取next参数
next = request.query_params.get("next")
# 拼接qq登录的url的路径
oauth_qq = OauthQQ(state=next)
login_url = oauth_qq.get_login_url()
# 返回url
return Response({'login_url': login_url})
class QQAuthUserView(CreateAPIView):
"""
通过QQ按钮登录的视图
"""
serializer_class = OAuthQQUserSerializer
def get(self, request):
# 获取code
code = request.query_params.get('code')
if not code:
return Response({"meaasge": "缺少code"}, status=status.HTTP_400_BAD_REQUEST)
oauth_qq = OauthQQ()
try:
# 根据code,获取access_token
access_token = oauth_qq.get_access_token(code)
# 根据access_token 获取openid
openid = oauth_qq.get_openid(access_token)
except OaAuthQQAPIError:
return Response({"message": "访问QQ接口异常"}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
# 根据openid查询数据库,判断用户是否存在
try:
oauth_qq_user = OauthQQUser.objects.get(openid=openid)
except OauthQQUser.DoesNotExist:
# 如果数据不存在,处理openid(加密)并返回
new_access_token = oauth_qq.generate_user_bind_access_token(openid)
return Response({"access_token": new_access_token})
else:
# 如果数据存在,表示用户已经绑定过身份,签发JWT_token
# 签发jwt token
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
user = oauth_qq_user.user
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return Response({
"username": user.username,
"user_id": user.id,
"token": token
})