Django项目QQ登录后端接口实现

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
            })


猜你喜欢

转载自blog.csdn.net/qq_38923792/article/details/91456881