所有第三方登录都要先获取第三方应用的Id和秘钥
开发之前,需要前往第三方登录的开发者平台QQ、新浪微博、Github,注册账号并填写信息申请接入,成功后会给你一个ID和秘钥,以后你就通过该ID和秘钥来获取令牌,从而实现第三方登录。申请ID和秘钥时Github不需要审核,很简单,而QQ和新浪微博需要审核,稍微麻烦一点。
获得ID和秘钥后需要在setting中进行设置:
以QQ登录为例:关键点就是获取到QQ的凭证openid,然后将openid跟我们的用户模型联系起来。
若想实现QQ登录,需要成为QQ互联的开发者,审核通过才可实现。
这是QQ登录的开发者文档:http://wiki.connect.qq.com/准备工作_oauth2-0 第三方登录都有其相应的开发者文档。
使用QQ登录的流程:
新创建一个模型类用于记录用户与第三方登录ID的关联关系:
创建模型的时候可以设置db_index=True来自动创建索引。
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
其中继承的BaseModel是创建的一个模型类基类,专门用于记录模型的创建更新时间。
这种模型类基类的创建需要添加方法进行说明:
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的表
模型类有了之后我们现在就开始完成QQ登录的具体逻辑:
按照一般第三方登录的风格需要完成三个接口的设计:
- 取得QQ的授权code
- 利用code取得唯一表示openid
- 将openid跟用户绑定并登录
一、返回QQ登录网址的视图
目的:访问QQ第三方的登录网址,对用户QQ身份进行校验,校验成功后返回授权code。期间需要利用next参数携带登录成功后的跳转页面的数据。
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})
python提供了标准模块urllib可以帮助我们发送HTTP请求,其中的
urllib.parse.urlencode(query):可以将query字典转换为url路径中的查询字符串,用途一般是将得到的返回结果拼接到url中。
urllib.request.urlopen(url, data=None):(爬虫要学的)发送http请求,如果data为None,发送GET请求,如果data不为None,发送POST请求
这一步逻辑是跳转到QQ的第三方登录页面,登录成功后会返回授权code并重定向到callback网址,这个callback网址是在注册QQ开发者是设置好的。
二、QQ登录回调处理
获取url中携带的参数使用“ request.query_params.get(‘参数名’) ” 例如:
code = request.query_params.get('code')
此处完成逻辑有:
- 通过路由携带的参数获取code得到用户数据
- 得到openid
- 根据openid判断用户是否已经存在不存在则创建
- 存在就进一步判断是否已经绑定,如绑定则生成token后直接跳转到指定页面
- 没有绑定则跳转到绑定页面(即下一个接口)
具体视图:
class QQAuthUserView(APIView):
"""
QQ登录的用户
"""
def get(self, request):
"""
获取qq登录的用户数据
"""
code = request.query_params.get('code')
if not code:
return Response({'message': '缺少code'}, status=status.HTTP_400_BAD_REQUEST)
oauth = OAuthQQ()
# 获取用户openid
try:
access_token = oauth.get_access_token(code)
openid = oauth.get_openid(access_token)
except QQAPIError:
return Response({'message': 'QQ服务异常'}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
# 判断用户是否存在
try:
qq_user = OAuthQQUser.objects.get(openid=openid)
except OAuthQQUser.DoesNotExist:
# 用户第一次使用QQ登录
token = oauth.generate_save_user_token(openid)
return Response({'access_token': token})
else:
# 找到用户, 生成token
user = qq_user.user
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
三、绑定用户身份接口
主要逻辑使用序列化器实现:
class OAuthQQUserSerializer(serializers.ModelSerializer):
"""
保存QQ用户序列化器
"""
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_save_user_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:
# 如果用户不存在,创建用户,绑定openid(创建了OAuthQQUser数据)
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