上篇已经介绍了QQ第三方登录的流程分析和模型类的创建,并且也知道了再整个过程中我们需要提供哪些API为前端提供数据。
一、提供用户登录URL的API实现
在上篇我们已经分析了当用户点击QQ登录按钮时,后端需要为前端提供进行QQ登录的URL,可能许多人会疑惑为什么不直接由前端处理URL,直接是由该URL进行进行如QQ登入界面?
这是由于我们需要根据QQ开发者文档提供相应的地址,和查询字符串等。
而由于第三方登录在其他的项目中,可能也会使用到第三方登录(QQ登录),所以我们需要考虑解耦的性能,这里我们定义了一个utils.py,用来存放 获取QQ登录URL、获取授权证书sccess_token、获取QQ用户的openid、以及生成绑定用户的token、和检测绑定用户的token。其代码如下:
QQ登录的辅助工具类
from django.conf import settings import urllib.parse from urllib.request import urlopen import logging import json from itsdangerous import TimedJSONWebSignatureSerializer as TJWSSerializer, BadData from . import constants from .exceptions import OAuthQQAPIError 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): ‘‘‘获取授权证书‘‘‘ 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 = urlopen(url) # 读取响应体数据 resp_data = resp.read() # bytes resp_data = resp_data.decode() # str # access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14 # 解析 access_token resp_dict = urllib.parse.parse_qs(resp_data) except Exception as e: logger.error(‘获取access_token异常:%s‘ % e) raise OAuthQQAPIError else: access_token = resp_dict.get(‘access_token‘) return access_token[0] def get_openid(self, access_token): ‘‘‘根据授权证书去获取用户的openid‘‘‘ url = ‘https://graph.qq.com/oauth2.0/me?access_token=‘ + access_token try: # 发送请求 resp = urlopen(url) # 读取响应体数据 resp_data = resp.read() # bytes resp_data = resp_data.decode() # str # callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} )\n; # 解析 resp_data = resp_data[10:-4] resp_dict = json.loads(resp_data) except Exception as e: logger.error(‘获取openid异常:%s‘ % e) raise OAuthQQAPIError else: openid = resp_dict.get(‘openid‘) return openid def generate_bind_user_access_token(self, openid): ‘‘‘根据openid生成进入绑定用户页面的token‘‘‘ serializer = TJWSSerializer(settings.SECRET_KEY, constants.BIND_USER_ACCESS_TOKEN_EXPIRES) token = serializer.dumps({‘openid‘: openid}) return token.decode() @staticmethod def check_bind_user_access_token(access_token): ‘‘‘检测用户携带进行绑定操作的token‘‘‘ serializer = TJWSSerializer(settings.SECRET_KEY, constants.BIND_USER_ACCESS_TOKEN_EXPIRES) try: data = serializer.loads(access_token) except BadData: return None else: return data[‘openid‘]
视图
# url(r‘^qq/authorization/$‘, views.QQAuthURLView.as_view()), class QQAuthURLView(APIView): """ 获取QQ登录的url ?next=xxx """ def get(self, request): # 获取next参数 next = request.query_params.get("next") # 拼接QQ登录的网址 oauth_qq = OAuthQQ(state=next) login_url = oauth_qq.get_login_url() # 返回 return Response({‘login_url‘: login_url})
这里,通过直接获取到进行QQ登录的URL,直接返回给前端。
二、QQ登录的回调处理的API实现
从上篇的QQ登录流程分析,我们可以知道当用户进入QQ登入页面时,进行了QQ登入认证,随后QQ服务器会根据我们提供的回调地址将页面重定向到该页面,而在进入该页面时,我们需要进行一下验证:
- 用户是否第一次进行QQ登入,即是否在数据库中与本项目的账号进行了绑定。
- 若进行了绑定,我们直接让其从哪里来回哪里去,即重定向到其进入QQ登入之前页面的HTML页面中,即在state中保存的URI中。
- 若没有进行绑定,则为其生成access_token,跳转到与本项目的账号进行绑定的页面。
故其业务逻辑代码如下:
视图:请求方式 : GET /oauth/qq/user/?code=xxx
class QQAuthUserView(APIView): """ QQ登录的用户 ?code=xxxx """ 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_qq = OAuthQQ() try: # 凭借code 获取access_token access_token = oauth_qq.get_access_token(code) # 上述的utils.py辅助工具类中方法 # 凭借access_token获取 openid openid = oauth_qq.get_openid(access_token) except OAuthQQAPIError: return Response({‘message‘: ‘访问QQ接口异常‘}, status=status.HTTP_503_SERVICE_UNAVAILABLE) # 根据openid查询数据库OAuthQQUser 判断数据是否存在 try: oauth_qq_user = OAuthQQUser.objects.get(openid=openid) except OAuthQQUser.DoesNotExist: # 如果数据不存在,处理openid 并返回 access_token = oauth_qq.generate_bind_user_access_token(openid) return Response({‘access_token‘: access_token}) else: # 如果数据存在,表示用户已经绑定过身份, 签发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 })
注:这里返回给前端生成的access_token,是通过Django的一个扩展类实现的 ---》
pip install itsdangerous
至于为什么要生成 token,而不是直接返回openid?
主要是由于:
当用户进行绑定时,若携带的openid过来进行绑定,我们无法知道用户携带的该openid是否为其之前进行QQ登录时,后端向QQ服务器获取的openid,若用户进行篡改成别的QQ用户的openid,那么我们进行绑定时,便会将别的QQ用户的openid与本项目的账号进行了绑定。
那么有没有解决方法呢?就是利用itsdangerous包生成的jwt_token,这种生成的token分为三个部分:header、payload(存放用户的某些信息)、以及signature,而signature是由前两个者配合本项目的secret_key进行生成的,故当用户拿到token来进行验证时,服务器这边会将token的header和payload取出并且再配合 secret_key生成 signature②,与用户携带过来的signature进行对对,这样用户若修改了token我们也可以发现。
三、绑定用户身份接口
业务逻辑分析:
- 用户需要填写手机号、密码、图片验证码、短信验证码、并且携带token
- 如果用户未在本项目注册过,则会将手机号作为用户名为用户创建一个本项目的账户,并绑定用户
- 如果用户已在本项目注册过,则检验密码后直接绑定用户
其流程图如下:
其代码逻辑如下:
class QQAuthUserView(CreateAPIView): """ QQ登录的用户 ?code=xxxx """ serializer_class = OAuthQQUserSerializer 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_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 OAuthQQAPIError: return Response({‘message‘: ‘访问QQ接口异常‘}, status=status.HTTP_503_SERVICE_UNAVAILABLE) # 根据openid查询数据库OAuthQQUser 判断数据是否存在 try: oauth_qq_user = OAuthQQUser.objects.get(openid=openid) except OAuthQQUser.DoesNotExist: # 如果数据不存在,处理openid 并返回 access_token = oauth_qq.generate_bind_user_access_token(openid) return Response({‘access_token‘: access_token}) else: # 如果数据存在,表示用户已经绑定过身份, 签发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 })
我们本可以定义一个视图post去实现,但是这里我们修改了上述的继承类 APIView 为 CreateAPIView,关于该类便不再详细的解析,它多继承了 CreateModelMixin 、GenericAPIView,只需要定义一个指明序列化器类即可,
serializer_class = OAuthQQUserSerializer
而其内部提供了视图方法 -- post方法,这里不再复述。随后定义一个序列化器。
from django_redis import get_redis_connection from rest_framework import serializers from rest_framework_jwt.settings import api_settings from users.models import User from .utils import OAuthQQ from .models import OAuthQQUser 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‘, ‘id‘, ‘username‘, ‘token‘) extra_kwargs = { ‘username‘: { ‘read_only‘: True }, ‘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 = OAuthQQ.check_bind_user_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) 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 = User.objects.create_user(username=mobile, mobile=mobile, password=password) 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 = token return user
关于QQ第三方登录便介绍到此,关键不是代码,而是业务逻辑思维~~~~~~
原文地址:https://www.cnblogs.com/littlefivebolg/p/9772958.html