小程序登錄注冊頁面
該頁面只有一個按鈕,如果用戶沒有注冊過的話,會直接完成注冊,首先用戶點擊獲取驗證碼按鈕,在小程序前端會對手機號做一個簡單的校驗,然后將手機號發送到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接口設計風格,獲取驗證碼和登錄注冊使用同一個視圖類的不同函數響應。
-
小程序發送獲取驗證碼的請求
-
發送短信。
Django接受到了請求,先校驗手機號是否合法
校驗失敗則返回{"status":False,"message":"手機號格式錯誤"}
,
校驗通過隨機生成一個驗證碼則通過騰訊雲發送短信,
如果發送失敗,則向前端返回{"status":False,"message":"短信發送失敗"}
如果發送成功,則以電話為鍵,驗證碼為值存入redis,向前端發送{"status":True,"message":"短信發送成功"}
-
小程序發送登錄注冊請求
-
完成登錄注冊
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
請看前面。