02-短信验证码完成登录注册功能

小程序登录注册页面

该页面只有一个按钮,如果用户没有注册过的话,会直接完成注册,首先用户点击获取验证码按钮,在小程序前端会对手机号做一个简单的校验,然后将手机号发送到Django后端,后端对此手机号发送短信验证码,用户在规定的时间内输入验证码,点击登录注册按钮,小程序前端将手机号和验证码再次发送到后端进行校验,校验通过的话,本次登录注册功能完成。


本文每个细节处只展现关联代码,文末会展现整个项目的文件结构,以及文件完整内容。


接口设计

获取验证码

url: "http://127.0.0.1:8000/api/login/"
data: { phone: this.data.phone }
method: 'GET',
dataType: 'json'

登录与注册

url: "http://127.0.0.1:8000/api/login/",
data: { phone: this.data.phone, code: this.data.code },
method: 'POST',
dataType: 'json',

后端技术:

Django DRF Redis 腾讯云

业务流程

该功能采用Restful接口设计风格,获取验证码和登录注册使用同一个视图类的不同函数响应。

  1. 小程序发送获取验证码的请求

  2. 发送短信。

Django接受到了请求,先校验手机号是否合法
校验失败则返回{"status":False,"message":"手机号格式错误"}
校验通过随机生成一个验证码则通过腾讯云发送短信,
如果发送失败,则向前端返回{"status":False,"message":"短信发送失败"}
如果发送成功,则以电话为键,验证码为值存入redis,向前端发送{"status":True,"message":"短信发送成功"}

  1. 小程序发送登录注册请求

  2. 完成登录注册

Django接收请求,校验手机号和验证码是否正确
校验失败返回{"status": False, 'message': '手机号或者验证码错误'}
校验成功,返回{"status": True, "data": {"token": token, "phone": phone}}

发送短信验证码

发送短信验证码之前要先校验手机号格式,在这里我们只是校验手机号格式是否正确,属于轻量级校验,我们使用DRF的Serializer类来实现,使用虽然代码量变多了,但是为了保证统一的编程风格。

1. 创建序列化类

创建文件serializer.py专门用来存放序列化类,在该文件中创建类MessageSerializer

MessageSerializer

lass MessageSerializer(serializers.Serializer):
    '''
    用于给手机发送短信时验证手机号是否正确的序列化类
    '''

    phone = serializers.CharField(label='手机号', validators=[phone_validator, ])

因为后面的登录注册接口也需要用到手机号校验,因此这里把手机号校验写成了一个函数提取出来,然后将其放到了MessageSerializer的手机号字段校验规则中

phone_validator

def phone_validator(value):
    if not re.match(r"^(1[3|4|5|6|7|8|9])\d{9}$", value):
        raise ValidationError('手机号格式错误')

2. 视图中引用序列化类的校验功能

在views.py中创建登录注册的视图处理类

LoginOrRegistView

class LoginOrRegistView(APIView):
    '''
    首先使用短信验证码请求校验类校验手机号是否合格
    合格则生成随机验证码,尝试发送短信,
    发送失败,就返回,发送成功就将手机号和短信验证码放入到redis中然后返回
    '''
    def get(self, request, *args, **kwargs):
        # print(request.query_params)
        ser = serializer.MessageSerializer(data=request.query_params)
        if not ser.is_valid():
            return Response({"status": False, "message": "手机号格式错误"})

        phone = ser.validated_data.get('phone')
        code = str(random.randint(100000, 999999))

        result = send_message(phone, code)
        if not result:
            return Response({"status": False, "message": "短信发送失败"})

        conn = get_redis_connection()
        conn.set(phone, code, ex=60*int(settings.TENCENT_LIMIT_TIME))

        return Response({"status": True, "message": "短信发送成功"})

上面的代码可以看到,我将腾讯云的发送短信功能提炼成了一个函数,直接调用即可,这里有我的关于如何使用腾讯云发送短信

3. 使用腾讯云发送短信

腾讯云发送短信函数可以写在项目根目录下的utils中。发送短信只需要一个手机号和验证码,其余的信息可以配置

tencent_sms.py

import json
from tencentcloud.common import credential
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.sms.v20210111 import sms_client, models
from paimai import settings # paimai是我本次项目的名字,请按照自己的修改


def send_message(phone, code):
    try:
        cred = credential.Credential(
            settings.TENCENT_SECRET_ID, settings.TENCENT_SECRET_KEY)

        httpProfile = HttpProfile()
        httpProfile.endpoint = settings.TENCENT_ENDPOINT

        clientProfile = ClientProfile()
        clientProfile.httpProfile = httpProfile
        client = sms_client.SmsClient(
            cred, settings.TENCENT_CITY, clientProfile)

        req = models.SendSmsRequest()
        params = {
            "PhoneNumberSet": [settings.TENCENT_CHINA + phone, ],
            "SmsSdkAppId": settings.TENCENT_APP_ID,
            "SignName": settings.TENCENT_SIGN,
            "TemplateId": settings.TENCENT_TEMPLATED_ID,
            "TemplateParamSet": [code, settings.TENCENT_LIMIT_TIME]
        }
        req.from_json_string(json.dumps(params))

        resp = client.SendSms(req)
        if resp.SendStatusSet[0].Code == "Ok":
            return True
        # print(resp.to_json_string(indent=2))

    except TencentCloudSDKException as err:
        # print(err)
        pass

该函数的具体内容请在参照使用腾讯云发送短信自行揣摩,这里不再赘述,我将其用到的一些配置信息写进了全局配置文件中

在项目的全局配置文件中添加如下配置

# ############################# 腾讯云短信配置 #############################
TENCENT_SECRET_ID = "你的secretid"
TENCENT_SECRET_KEY = "你的secretkey"
TENCENT_CITY = "ap-guangzhou"
TENCENT_APP_ID = "1400636319"
TENCENT_SIGN = "派森之旅个人公众号"
TENCENT_TEMPLATED_ID = "1310476"
TENCENT_ENDPOINT = "sms.tencentcloudapi.com"
TENCENT_LIMIT_TIME = "2" # 其中一个配置发送模板的配置参数,在我的模板中,这个参数填写之后,是,请与2分钟之内完成登录和注册
TENCENT_CHINA = "+86"

4. 配置redis

步骤2中可以看到这条代码get_redis_connection这是django提供的redis连接器,django可以通过配置的形式直接使用redis,而无需我们专门书写

在项目的全局配置文件settings.py中追加这段代码,读者请依据自己的redis配置修改填写

CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        # 需要着重修改的就是这个redis地址
        "LOCATION": "redis://192.168.1.100:6379",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            # "PASSwORD":“密码",
            "CONNECTION_POOL_KWARGS": {"max_connections": 100}
        }
    }
}

登录注册

用户收到短信验证码,填写之后,向后端发送登录注册请求,在这里我们要先后检验手机号和验证码是否正确,老规矩,但凡携带数据的请求,我们都尽可能得用序列化器来实现校验,而在核心代码区,则只需要调用一个is_valid()来判断即可,可使代码更加结构化和简洁。

1. 登录注册的序列化类LoginOrRegistSerializer

class LoginOrRegistSerializer(serializers.Serializer):
    '''
    登陆或者注册时的序列化类,
    首先校验手机号是否正确
    校验短信验证码格式是否正确[长度是否正确,字符是否都是数字]
    和redis中的验证码比较,如果redis中取不到值,则验证码过期
    如果和redis中的验证码不一致,则验证码输入错误
    '''
    phone = serializers.CharField(label='手机号', validators=[phone_validator, ])
    code = serializers.CharField(label='短信验证码')

    def validate_code(self, value):
        if len(value) != 6:
            raise ValidationError('验证码格式错误')
        if not value.isdecimal():
            raise ValidationError('验证码格式错误')

        phone = self.initial_data['phone']
        conn = get_redis_connection()
        code = conn.get(phone)

        if not code:
            raise ValidationError('验证码已过期')
        if value != code.decode('utf-8'):
            raise ValidationError('验证码错误')

        return value

2. 登录注册实现

还是LoginOrRegistView这个视图类,不过这次我们使用post这个方法来响应请求,因为这个请求本身就是post类型的


class LoginOrRegistView(APIView):

    def post(self, request, *args, **kwargs):
        # print(request.data)
        ser = serializer.LoginOrRegistSerializer(data=request.data)
        if not ser.is_valid():
            print(ser.errors)
            return Response({"status": False, 'message': '手机号或者验证码错误'})

        phone = ser.validated_data.get('phone')
        user_object, flag = UserInfo.objects.get_or_create(phone=phone)
        user_object.token = str(uuid.uuid4())
        user_object.save()

        return Response({"status": True, "data": {"token": user_object.token, "phone": phone}})

可以看到这里的post方法内容比之前面的get方法还要少,使用了序列化类以后,代码更加精炼简洁,功能划分也更清楚了。

上述代码中,校验成功后,后端使用uuid随机生成一个token,也可以使用jwt,这里为了方便,我们就使用了比较原始的token验证,将touke放在用户表中,在用户登录以后就通过token来实现用户请求的身份识别。get_or_create()这个方法,表中有就返回,没有就创造,也就是我们这里实现的没有注册的话默认注册。用户表的涉及到django的model使用,在我的系列博客中有过讲解,这里不做说明,表字段自行设计

总结

使用小程序+django+腾讯云完成验证码登录注册的核心代码就这么多,至于小程序端的代码为什么没有写?因为这是一个前后端分离的项目,后端只需要处理请求即可,也就是说你就算使用postman也可以完成这段后端代码的开发和测试。前端自有逻辑,写在这里太冗余了。本系列博客并非是从零搭建项目,而是记录每一个功能模板的实现。

仔细测试后端登录注册部分,可以发现,有几种异常情况,我们统统返回了手机号或者验证码错误,这对用户来说不友好,例如是手机号错误,还是验证码错误,验证码错误又分为,验证码为空,验证码格式错误和验证码过期。验证码过期的时候,是从redis中取不到数据的,但是用户是否请求过验证码呢?这些是否有必要分的那么细?这就视项目情况而定。这些都有技术手段可以搞定,这里不过分阐述了。

相对完成的代码

1. 文件结构

2. 全局配置文件setting.py

# 使用DRF以及自己定义的app需要导入
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'api'
]

# Redis配置
CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://192.168.1.100:6379",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            # "PASSwORD":“密码",
            "CONNECTION_POOL_KWARGS": {"max_connections": 100}
        }
    }
}

# ############################# 腾讯云短信配置 #############################
TENCENT_SECRET_ID = "AKIDjGWVDfqkNkpdAPP9cokPmshGYYHjvpSp"
TENCENT_SECRET_KEY = "7xTkjSv61f27wTjiQr1uWsxlHXVDeohI"
TENCENT_CITY = "ap-guangzhou"
TENCENT_APP_ID = "1400636319"
TENCENT_SIGN = "派森之旅个人公众号"
TENCENT_TEMPLATED_ID = "1310476"
TENCENT_ENDPOINT = "sms.tencentcloudapi.com"
TENCENT_LIMIT_TIME = "2"
TENCENT_CHINA = "+86"

3. 项目路由 pai.urls.py

from django.contrib import admin
from django.urls import path, re_path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    re_path('^api/', include('api.urls'))
]

4. 项目下的应用api.urls.py

from django.urls import re_path, include
from api import views

urlpatterns = [
    re_path(r'^login/', views.LoginOrRegistView.as_view())
]

5. 序列化器类api.serializer.py

import re

from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from django_redis import get_redis_connection


def phone_validator(value):
    if not re.match(r"^(1[3|4|5|6|7|8|9])\d{9}$", value):
        raise ValidationError('手机号格式错误')


class MessageSerializer(serializers.Serializer):
    '''
    用于给手机发送短信时验证手机号是否正确的序列化类
    '''

    phone = serializers.CharField(label='手机号', validators=[phone_validator, ])


class LoginOrRegistSerializer(serializers.Serializer):
    '''
    登陆或者注册时的序列化类,
    首先校验手机号是否正确
    校验短信验证码格式是否正确[长度是否正确,字符是否都是数字]
    和redis中的验证码比较,如果redis中取不到值,则验证码过期
    如果和redis中的验证码不一致,则验证码输入错误
    '''
    phone = serializers.CharField(label='手机号', validators=[phone_validator, ])
    code = serializers.CharField(label='短信验证码')

    def validate_code(self, value):
        if len(value) != 6:
            raise ValidationError('验证码格式错误')
        if not value.isdecimal():
            raise ValidationError('验证码格式错误')

        phone = self.initial_data['phone']
        conn = get_redis_connection()
        code = conn.get(phone)

        if not code:
            raise ValidationError('验证码已过期')
        if value != code.decode('utf-8'):
            raise ValidationError('验证码错误')

        return value

6. 视图类api.views.py

import random
import uuid

from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework.response import Response
from django_redis import get_redis_connection

from utils.tencent_sms import send_message
from paimai import settings
from api import serializer
from api.models import UserInfo


class LoginOrRegistView(APIView):

    def post(self, request, *args, **kwargs):
        # print(request.data)
        ser = serializer.LoginOrRegistSerializer(data=request.data)
        if not ser.is_valid():
            print(ser.errors)
            return Response({"status": False, 'message': '手机号或者验证码错误'})

        phone = ser.validated_data.get('phone')
        user_object, flag = UserInfo.objects.get_or_create(phone=phone)
        user_object.token = str(uuid.uuid4())
        user_object.save()

        return Response({"status": True, "data": {"token": user_object.token, "phone": phone}})

    def get(self, request, *args, **kwargs):
        # print(request.query_params)
        ser = serializer.MessageSerializer(data=request.query_params)
        if not ser.is_valid():
            return Response({"status": False, "message": "手机号格式错误"})

        phone = ser.validated_data.get('phone')
        code = str(random.randint(100000, 999999))

        result = send_message(phone, code)
        if not result:
            return Response({"status": False, "message": "短信发送失败"})

        conn = get_redis_connection()
        conn.set(phone, code, ex=60*int(settings.TENCENT_LIMIT_TIME))

        return Response({"status": True, "message": "短信发送成功"})

7. 应用的模型类 api.models.py

from django.db import models


class UserInfo(models.Model):
    phone = models.CharField(verbose_name='手机号', max_length=11, unique=True)

    token = models.CharField(verbose_name='用户TOKEN',
                             max_length=64, null=True, blank=True)

8. 腾讯云发送短信函数utils.tencent_sms.py

请看前面。

posted @   yaowy  阅读(1274)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示