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

請看前面。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM