用户模块---QQ登陆
流程图
QQ登陆文档:http://wiki.connect.qq.com/%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C_oauth2-0前端
流程简述:
1.当点击qq登陆图标时,进入生成登陆url的接口
2.在前端的回调函数中跳转到qq登陆的扫码页面
3.扫码登录后,qq会携带code访问申请时指定的回调地址,
使用code获取access_token
使用access_token最终获取openid
4用来判断用户是不是第一次使用QQ登陆
5.是,返回绑定/注册,页面第一次使用QQ登陆,额外判断,是否已经注册帐号
5.1是,统一跳转到绑定/注册界面,
5.1.1将openid和帐号进行绑定/注册,手动调用jwt功能返回jwt,id,usernamepython
5.2否,返回首页,手动调用jwt功能返回jwt,id,usernameredis
具体实现:django
使用到的模块:json
import urllib urllib.parse.urlencode(query) # 将字典转换为url路径中的查询字符串 urllib.parse.parse_qs(qs) # 将查询字符串格式数据转换为python的字典 urllib.request.urlopen(url, data=None) 在python后端发起http请求, 若是data为None,发送GET请求, 若是data不为None,发送POST请求 response.read().decode() 返回response响应对象,能够经过read()读取响应体数据,须要注意读取出的响应体数据为bytes类型
手动调用jwt生成token用于验证登陆状态后端
from rest_framework_jwt.settings import api_settings jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER payload = jwt_payload_handler(user) jwt_token = jwt_encode_handler(payload)
itsdangerous模块用于生成token和解析tokenapi
from itsdangerous import TimedJSONWebSignatureSerializer serializer = TimedJSONWebSignatureSerializer(settings.SECRET_KEY, 300) # 须要转换成token的数据 data = {'openid': openid} # .dumps生成token,bytes类型,须要解码 token = serializer.dumps(data).decode() # .loads将token解析为须要的数据, openid = serializer.loads(token).get('openid')
模型类,保存QQ的openid和本站用户之间的关系:函数
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: # 指定BaseModel为抽象类,不会建立实体表 abstract = True class OAuthQQUser(BaseModel): """QQ登陆的模型""" openid = models.CharField(max_length=64, verbose_name='openid', db_index=True) user = models.ForeignKey('user.User', on_delete=models.CASCADE, verbose_name='用户') objects = models.Manager() class Meta: db_table = 'tb_oauth_qq' verbose_name = 'QQ登陆用户数据' verbose_name_plural = verbose_name
定义一个工具类,主要负责生成token和验证token是否正确工具
class OAuthQQ(object): """用于qq登陆的工具类""" def __init__(self, client_id=None, client_secret=None, redirect_uri=None, state=None): """QQ登陆开发文档中须要的参数""" 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 # 用于保存登陆成功后的跳转页面路径
在OAuthQQ工具类中新增生成登陆url的方法post
def generate_qq_login_url(self): """生成用于qq登陆扫码的url地址""" params = { 'response_type': 'code', # 默认值 'client_id': self.client_id, 'redirect_uri': self.redirect_uri, 'state': self.state, 'scope': 'get_user_info', # 用户勾选的受权范围,get_user_info表示,获取登陆用户的昵称、头像、性别 } url = 'https://graph.qq.com/oauth2.0/authorize?' # 拼接查询字符串, url += parse.urlencode(params) return url
定义返回扫码QQ登陆url的接口
# 在点击qq登陆图标时向接口发起请求 # 后端生成用于QQ扫码登陆的页面的url地址 # 在前端回调函数中执行 # GET /oauth/qq/authorization/?state=xxx class QQAuthUrlView(APIView): """获取QQ扫码登陆的网址接口""" def get(self, request): """ :return 扫码的url地址 """ state = request.query_params.get('state') oauthqq = OAuthQQ(state=state) qq_login_url = oauthqq.generate_qq_login_url() return Response({'qq_login_url': qq_login_url})
前端将code当参数传入后端接口,生成获取access_token的url
在OAuthQQ工具类新增,使用code请求并获取QQ的access_token,的方法
def get_qq_access_token(self, code): """获取access_token""" params = { 'grant_type': 'authorization_code', 'client_id': self.client_id, 'client_secret': self.client_secret, 'code': code, 'redirect_uri': self.redirect_uri, } url = 'https://graph.qq.com/oauth2.0/token?' url += parse.urlencode(params) # 向qq方发起http请求,获取包含access_token的查询字符串 # 形式access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14 try: response = request.urlopen(url) response_data = response.read().decode() # 讲查询字符串转换为python中的字典,[{}] data = parse.parse_qs(response_data) access_token = data.get('access_token', None)[0] except Exception as e: logger.error(e) raise Exception('获取access_token异常') return access_token
根据access_token生成获取openid的url
后端发送http请求,从返回值中获取openid
def get_qq_openid(self, access_token): """获取openid""" url = 'https://graph.qq.com/oauth2.0/me?access_token=' url += access_token logger.error(url) try: response = request.urlopen(url) response_data = response.read().decode() # 返回一个字符串 callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} )\n; data_dict = json.loads(response_data[10:-4]) openid = data_dict.get('openid', None) except Exception as e: logger.error(e) raise Exception('获取openid异常') return openid
根据openid去判断,该用户是不是第一次使用QQ登陆功能.
定义类视图和serializers序列化器
class QQAuthUserView(GenericAPIView): """QQ登陆后接口""" serializer_class = OAuthQQUserSerializer def get(self, request): """QQ登陆""" code = request.query_params.get('code') if not code: return Response({'message': 'code不存在'}, 400) # 目标是经过 code获取access_token oauthqq = OAuthQQ() access_token = oauthqq.get_qq_access_token(code) # 经过access_token获取openid openid = oauthqq.get_qq_openid(access_token) # 获取openid后须要判断 # oauthqquser = OAuthQQUser.get try: oauthqquser = OAuthQQUser.objects.get(openid=openid) except Exception as e: logger.error('此人未绑定或未注册:%s' % e) # 1.第一次用qq登陆 # 使用openid生成记录qq身份的token,以便注册或绑定时验证身份 access_token = OAuthQQ.generate_save_user_token(openid) return Response({'access_token': access_token}) # 1.1 已经注册本站帐号--->跳转绑定界面 # 1.2 未注册本站帐号--->注册并绑定 # 2.之前已经qq登陆过(必定有本站帐号) else: user = oauthqquser.user # 生成jwt_token,用于记录登陆状态 jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER payload = jwt_payload_handler(user) jwt_token = jwt_encode_handler(payload) data = { 'user_id': user.id, 'username': user.username, 'token': jwt_token } return Response(data=data) def post(self, request): """QQ帐号绑定和新增功能""" serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) user = serializer.save() jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER payload = jwt_payload_handler(user) jwt_token = jwt_encode_handler(payload) data = { 'user_id': user.id, 'username': user.mobile, 'token': jwt_token } return Response(data=data)
序列化器
class OAuthQQUserSerializer(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 = OAuthQQ.check_token_by_openid(access_token) if not openid: raise serializers.ValidationError('access_token失效') # 检验短信验证码 mobile = data['mobile'] sms_code = data['sms_code'] redis_conn = get_redis_connection('verify_codes') real_sms_code = redis_conn.get('sms_%s' % mobile).decode() if not real_sms_code: raise serializers.ValidationError('短信验证码失效或过时') if real_sms_code != sms_code: raise serializers.ValidationError('短信验证码错误') # 若是用户存在,检查用户密码 try: user = User.objects.get(mobile=mobile) except Exception as e: logger.error('本站用户不存在,等待注册---%s' % e) pass else: # 若是存在就校验密码 password = data['password'] if not user.check_password(password): raise serializers.ValidationError('密码错误') data['user'] = user data['openid'] = openid return data def create(self, validated_data): user = validated_data.get('user', None) if not user: # 用户不存在,先注册本站新用户 user = User.objects.create_user( username=validated_data['mobile'], password=validated_data['password'], mobile=validated_data['mobile'], ) # 新老用户都绑定QQ的openid OAuthQQUser.objects.create( openid=validated_data['openid'], user=user ) return user