用户授权包括几个步骤:
1)弹出登录页面,不提供单独的接口,用户访问其它页面时,如果没注册,则直接进入登录页面。
需要注意的是:如果用户在公众号里访问其它页面,弹出登录页面时,会自动获取用户的微信信息,如头像、openid等。后端把这些用户信息一并发送到前端。如果用户在电脑浏览器上访问网页,则不获取用户其它信息。
2)用户输入手机号,点击发送验证码。
3)后台验证手机号,如果手机号合法,则返回一个Access_token。
4)前端带上Access_token请求发送验证码,后端验证Access_token后,调用短信接口,发送验证码。
5)用户收到并输入验证码,点击确定,前端把手机号、验证码和用户的其它信息(指微信登录的信息)一并发送到后端。.
6)后端收到验证码,并验证验证码,确认无误后,保存用户信息,同时在前端session里保存用户Access_token,并重定向到程序首页。
7)以后,前端每次访问,都会携带上Access_token,后端验证身份后,直接让用户登录,不需要用户再输入任何信息,只有当Access_token过期后,用户需要重新获取授权。
1 根据手机号,获取短信身份令牌
在Organization/views下创建GetSMSToken.py文件,内容如下:
from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from TongHeng2 import settings from GeneralTools.CustomSchema import CustomSchema from GeneralTools import Constents from coreapi import Field from coreschema import String from itsdangerous import TimedJSONWebSignatureSerializer as TJWSSerializer from GeneralTools.Verifications import mobileVerify class GetSMSToken(APIView): schema = CustomSchema( manual_fields={ 'get': [ Field(name="mobile", required=True, location="query", schema=String(description='手机号')), ], } ) @classmethod def get(cls, request): """ 【功能描述】用户注册第一步:根据手机号生成验证码身份令牌</br> 【返回参数】</br> mobile:手机号</br> access_token:短信身份令牌</br> """ mobile = request.query_params.get('mobile') if not mobile: return Response(data={'message': '缺少mobile参数'}, status=status.HTTP_400_BAD_REQUEST) # 验证手机号码格式 if not mobileVerify(mobile): return Response(data={'message': '手机号格式错误'}, status=status.HTTP_400_BAD_REQUEST) # 创建itsdangerous模型的转换工具 tjwserializer = TJWSSerializer( secret_key=settings.SECRET_KEY, salt=Constents.SALT, expires_in=Constents.VERIFY_ACCESS_TOKEN_EXPIRES ) access_token = tjwserializer.dumps({'mobile': mobile}) # bytes access_token = access_token.decode() # str data = { 'mobile': mobile, 'access_token': access_token } return Response(data=data, status=status.HTTP_200_OK)
2 根据用户上传身份令牌,发送验证码
在Organization/views下创建SendSMS.py
from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from TongHeng2 import settings from GeneralTools.CustomSchema import CustomSchema from coreapi import Field from coreschema import String from itsdangerous import TimedJSONWebSignatureSerializer as TJWSSerializer from GeneralTools.Redis import get_redis_connection import random from GeneralTools import Constents from qcloudsms_py import SmsSingleSender from qcloudsms_py.httpclient import HTTPError import logging logger = logging.getLogger('tongheng2') def send_sms_code(mobile, param): """ 发送短信验证码 :return: 错误码,0表示成功 """ sms_code = '%06d' % random.randint(0, 999999) # 缓存验证码到Redis conn = get_redis_connection("sms_codes") # 使用Redis的pipeline管道一次执行多个命令 pl = conn.pipeline() pl.setex("sms_%s" % mobile, Constents.SMS_CODE_REDIS_EXPIRES, sms_code) pl.setex("send_flag_%s" % mobile, Constents.SEND_SMS_CODE_INTERVAL, 1) # 让管道执行命令 pl.execute() sender = SmsSingleSender(Constents.SMS_SDK_APP_ID, Constents.SMS_APP_KEY) params = [param, sms_code, '5', mobile[7:]] # 【仝恒智能科技温馨提示】您正在手机注册,验证码为:1314,请于5分钟内填写。尾号(6960)验证 try: data = sender.send_with_param( nationcode=86, # 国别 phone_number=mobile, # 手机号 template_id=Constents.SMS_REGISTER_TEMPLATE_ID, # 短信模板ID params=params, # 发送参数 sign=Constents.SMS_SIGN, # 短信签名 extend="", ext="" ) logger.error("sms result:%s" % data) except HTTPError as e: logger.error(e) return 500 except Exception as e: logger.error(e) return 500 # {'result': 0, 'errmsg': 'OK', 'ext': '', 'sid': '2106:369348897515820777343026696', 'fee': 1} # result 是 number 错误码,0表示成功(计费依据),非0表示失败,更多详情请参见 错误码 # errmsg 是 string 错误消息,result 非0时的具体错误信息 # ext 否 string 用户的 session 内容,腾讯 server 回包中会原样返回 # fee 否 number 短信计费的条数,计费规则请参考 国内短信内容长度计算规则 或 国际/港澳台短信内容长度计算规则 # sid 否 string 本次发送标识 ID,标识一次短信下发记录 return data['result'] from itsdangerous import BadData class SendSMSCode(APIView): schema = CustomSchema( manual_fields={ 'get': [ Field(name="access_token", required=True, location="query", schema=String(description='access token')), ], } ) @classmethod def get(cls, request): """ 【功能描述】用户注册第二步:根据用户上传的身份令牌,发送短信验证码</br> GET /Organizations/SendSMSCode/?access_token=*** """ access_token = request.query_params.get('access_token') if not access_token: return Response(data={'message': '缺少access_token参数'}, status=status.HTTP_400_BAD_REQUEST) # 校验access_token tjwserializer = TJWSSerializer( secret_key=settings.SECRET_KEY, salt=Constents.SALT, expires_in=Constents.VERIFY_ACCESS_TOKEN_EXPIRES ) try: data = tjwserializer.loads(access_token) mobile = data['mobile'] # 60秒之内只允许发送一次验证码 # send_flag_<mobile> = 1,由Redis维护60秒有效期 conn = get_redis_connection("sms_codes") send_flag = conn.get("send_flag_%s" % mobile) if send_flag: return Response(data={'message': '发送短信次数过于频繁'}, status=status.HTTP_400_BAD_REQUEST) # 发送短信验证码 param = '手机注册' code = send_sms_code(mobile, param) if code == 0: data = {'message': 'ok'} else: return Response(data={'message': '发送短信验失败'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) except BadData as e: logger.error(e) return Response(data={'message': '无效access_token参数'}, status=status.HTTP_400_BAD_REQUEST) return Response(data=data, status=status.HTTP_200_OK)
3 用户注册
前端上传用户资料,如果该用户存在,则更新资料,如果不存在,则新增用户。
其中有几点需要注意:
1)序列化器只能选择普通序列化器,不能选择模型序列化器。
因为模型中存在mobile和openid有唯一索引,如果用模型序列化器,DRF会先验证模型中的约束,如果是修改,DRF会直接拦截返回记录已存在,故必须用普通序列化器。
2)validate_字段名验证,只有前端提交了这个字段,才会进行验证,如果没提交,是不会验证的。而前端如果没有数据,则不提交。
from rest_framework import serializers from rest_framework.response import Response from rest_framework import status import re from rest_framework_jwt.settings import api_settings from django_redis import get_redis_connection from django.contrib.auth.hashers import make_password, check_password import logging from rest_framework.generics import CreateAPIView, ListAPIView from Applications.Organizations.models import UserInfo from GeneralTools.Verifications import mobileVerify from django.db.models import Q from GeneralTools.AuthToken import getToken # 获取在配置文件中定义的logger,用来记录日志 logger = logging.getLogger('Organizations') class RegisterSerializer(serializers.Serializer): """ 用户注册序列化器 """ name = serializers.CharField(max_length=100, min_length=2, required=True, help_text='姓名') photo_url = serializers.ImageField(use_url='user/', required=False, help_text='头像') password = serializers.CharField(help_text='密码', required=False, write_only=True) mobile = serializers.CharField(min_length=11, max_length=11, help_text='手机号') openid = serializers.CharField(required=False, help_text='openid') # write_only 表明该字段仅用于反序列化输入,默认False sms_code = serializers.CharField(label='短信验证码', write_only=True, help_text='短信验证码', min_length=6, max_length=6) # read_only 表明该字段仅用于序列化输出,默认False # token = serializers.CharField(label='JWT Token', read_only=True, help_text='JWT Token') class Meta: model = UserInfo fields = ('id', 'name', 'photo_url', 'password', 'mobile', 'openid', 'sms_code', 'token') extra_kwargs = { 'id': { 'read_only': True, 'help_text': '用户ID' }, 'name': { 'help_text': '姓名' }, 'photo_url': { 'help_text': '头像' }, 'mobile': { 'min_length': 11, 'max_length': 11, 'error_messages': { 'min_length': '请输入11位有效手机号码', 'max_length': '请输入11位有效手机号码', }, 'help_text': '手机号' }, 'openid': { 'help_text': '微信openID' }, } @classmethod def validate_mobile(cls, value): """ 验证手机号 """ if not mobileVerify(value): raise serializers.ValidationError('手机号格式错误') return value @classmethod def validate_password(cls, value): """ 只要输入了密码,就必须符合密码规则 """ if len(value) < 6: raise serializers.ValidationError('密码不能少于6位') elif len(value) > 12: raise serializers.ValidationError('密码不能大于12位') return value def validate(self, attrs): # 因token是临时字段,不能触发validate_token函数,故只能写入validate # 2 验证验证码 mobile = attrs['mobile'] sms_code = attrs['sms_code'] conn = get_redis_connection("sms_codes") real_sms_code = conn.get("sms_%s" % mobile) if real_sms_code is None: raise serializers.ValidationError('短信验证码已过期') real_sms_code = real_sms_code.decode() if real_sms_code != sms_code: raise serializers.ValidationError('短信验证码错误') del attrs['sms_code'] return attrs def create(self, validated_data): user = UserInfo.objects.filter( Q(mobile=validated_data['mobile']) | Q(openid=validated_data['openid']) ).first() if user is not None: if validated_data.get('openid', None) is not None: user.openid = validated_data.get('openid') user.mobile = validated_data.get('mobile') user.name = validated_data.get('name', None) # 如果有头像,则更新头像,否则维持原有头像 if validated_data.get('photo_url', None) is not None: user.photo_url = validated_data.get('photo_url') # 如果有密码,就更新密码,否则维持原有密码 if validated_data.get('password', None) is not None: user.password = make_password(validated_data.get('password')) user.username = validated_data['mobile'] user.save() else: validated_data['username'] = validated_data['mobile'] if validated_data.get('password', None) is not None: validated_data['password'] = make_password(validated_data['password']) user = UserInfo.objects.create(**validated_data) self.context['request'].session['access_token'] = getToken(user.openid, user.mobile) return user def update(self, instance, validated_data): pass class Register(CreateAPIView): """ 【功能说明】用户注册第三步:创建用户 1 绑定手机 2 快捷登录 3 更改密码 """ serializer_class = RegisterSerializer