一、图形验证码
1.1 图形验证码逻辑分析
需要新建应用
verifications
知识要点
将图形验证码的文字信息保存到Redis数据库,为短信验证码做准备。
1.2 图形验证码接口设计和定义
1. 图形验证码后端接口设计
1.请求方式
选项 | 方案 |
---|---|
请求方法 | GET |
请求地址 | image_codes/(?P<uuid>[\w-]+)/ |
2.请求参数:路径参数
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
uuid | string | 是 | 唯一编号 |
3.响应结果:
image/jpeg
2. 图形验证码后端接口定义
1.图形验证码视图
from django.views import View
class ImageCodeView(View):
"""图形验证码"""
def get(self, request, uuid):
"""
:param request: 请求对象
:param uuid: 唯一标识图形验证码所属于的用户
:return: image/jpeg
"""
pass
2.总路由
urlpatterns = [
...
# 添加验证码 verifications 总路由
path('', include('apps.verifications.urls')),
]
3.子路由
from django.conf.urls import url
from . import views
urlpatterns = [
# 图形验证码
path('image_codes/<uuid:uuid>/', views.ImageCodeView.as_view()),
]
4.添加自定义转换器
class UUIDConverter:
"""自定义路由转换器去匹配手机号"""
# 定义UUID的正则表达式
regex = '[\w-]+'
def to_python(self, value):
# to_python:将匹配结果传递到视图内部时使用
return str(value)
# 在工程的总路由出添加
from utils.converters import UUIDConverter
from django.urls import register_converter
register_converter(UUIDConverter,'uuid')
1.3 图形验证码后端逻辑
1. 准备captcha扩展包
提示:
captcha
扩展包用于后端生成图形验证码
可能出现的错误
- 报错原因:环境中没有Python处理图片的库: PIL
解决办法
- 安装Python处理图片的库:
pip install Pillow
2. 准备Redis数据库
准备Redis的2号库存储验证码数据
"code": { # 验证码
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/2",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
},
3. 图形验证码后端逻辑实现
from django_redis import get_redis_connection
from libs.captcha.captcha import captcha
from django.views import View
from django.http import HttpResponse
class ImageCodeView(View):
"""图形验证码"""
def get(self, request, uuid):
"""
:param request: 请求对象
:param uuid: 唯一标识图形验证码所属于的用户
:return: image/jpeg
"""
# 生成图片验证码
text, image = captcha.generate_captcha()
# 保存图片验证码
redis_conn = get_redis_connection('code')
redis_conn.setex('img_%s' % uuid, 300, text)
# 响应图片验证码
return HttpResponse(image, content_type='image/jpeg')
二、短信验证码
2.1 短信验证码逻辑分析
知识要点
- 为了避免用户使用图形验证码恶意测试,我们提取了图形验证码后,立即删除Redis中的图形验证码。
- 保存短信验证码是为注册做准备的。
- Django不具备发送短信的功能,所以我们借助第三方的容联云通讯短信平台来帮助我们发送短信验证码。
2.1 容联云通讯短信平台
1. 容联云通讯短信平台介绍
1.容联云官网
- 容联云通讯网址: 容联云通讯_短信平台、手机验证码、语音验证码、IM即时通讯、云呼叫系统等互联网通信服务
- 注册并登陆
2.容联云管理控制台
3.容联云创建应用
4.应用申请上线,并进行资质认证
5.完成资质认证,应用成功上线
6.添加测试号码
7.短信模板
2. 容联云通讯短信SDK测试
1.模板短信SDK下载
2.模板短信SDK使用说明
3.集成模板短信SDK
CCPRestSDK.py
:由容联云通讯开发者编写的官方SDK文件,包括发送模板短信的方法ccp_sms.py
:调用发送模板短信的方法
4.模板短信SDK测试
ccp_sms.py
文件中
# -*- coding:utf-8 -*-
from libs.yuntongxun.CCPRestSDK import REST
# 说明:主账号,登陆云通讯网站后,可在"控制台-应用"中看到开发者主账号ACCOUNT SID
_accountSid = '8aaf070862181ad5016236f3bcc811d5'
# 说明:主账号Token,登陆云通讯网站后,可在控制台-应用中看到开发者主账号AUTH TOKEN
_accountToken = '4e831592bd464663b0de944df13f16ef'
# 请使用管理控制台首页的APPID或自己创建应用的APPID
_appId = '8aaf070868747811016883f12ef3062c'
# 说明:请求地址,生产环境配置成app.cloopen.com
_serverIP = 'sandboxapp.cloopen.com'
# 说明:请求端口 ,生产环境为8883
_serverPort = "8883"
# 说明:REST API版本号保持不变
_softVersion = '2013-12-26'
# 云通讯官方提供的发送短信代码实例
# 发送模板短信
# @param to 手机号码
# @param datas 内容数据 格式为数组 例如:{'12','34'},如不需替换请填 ''
# @param $tempId 模板Id
def sendTemplateSMS(to, datas, tempId):
# 初始化REST SDK
rest = REST(_serverIP, _serverPort, _softVersion)
rest.setAccount(_accountSid, _accountToken)
rest.setAppId(_appId)
result = rest.sendTemplateSMS(to, datas, tempId)
print(result)
for k, v in result.items():
if k == 'templateSMS':
for k, s in v.items():
print('%s:%s' % (k, s))
else:
print('%s:%s' % (k, v))
if __name__ == '__main__':
# 注意: 测试的短信模板编号为1
sendTemplateSMS('18310820688', ['123456', 5], 1)
5.模板短信SDK返回结果说明
{
'statusCode': '000000', // 状态码。'000000'表示成功,反之,失败
'templateSMS':
{
'smsMessageSid': 'b5768b09e5bc4a369ed35c444c13a1eb', // 短信唯一标识符
'dateCreated': '20190125185207' // 短信发送时间
}
}
3. 封装发送短信单例类
1.封装发送短信单例类
class CCP(object):
"""发送短信的单例类"""
def __new__(cls, *args, **kwargs):
# 判断是否存在类属性_instance,_instance是类CCP的唯一对象,即单例
if not hasattr(CCP, "_instance"):
cls._instance = super(CCP, cls).__new__(cls, *args, **kwargs)
cls._instance.rest = REST(_serverIP, _serverPort, _softVersion)
cls._instance.rest.setAccount(_accountSid, _accountToken)
cls._instance.rest.setAppId(_appId)
return cls._instance
2.封装发送短信单例方法
def send_template_sms(self, to, datas, temp_id):
"""
发送模板短信单例方法
:param to: 注册手机号
:param datas: 模板短信内容数据,格式为列表,例如:['123456', 5],如不需替换请填 ''
:param temp_id: 模板编号,默认免费提供id为1的模板
:return: 发短信结果
"""
result = self.rest.sendTemplateSMS(to, datas, temp_id)
if result.get("statusCode") == "000000":
# 返回0,表示发送短信成功
return 0
else:
# 返回-1,表示发送失败
return -1
3.测试单例类发送模板短信结果
if __name__ == '__main__':
# 注意: 测试的短信模板编号为1
CCP().send_template_sms('18310820688', ['123456', 5], 1)
4. 知识要点
- 容联云通讯只是发送短信的平台之一,还有其他云平台可用,比如,阿里云等,实现套路都是相通的。
- 将发短信的类封装为单例,属于性能优化的一种方案。
2.3 短信验证码后端逻辑
1. 短信验证码后端接口设计
1.请求方式
选项 | 方案 |
---|---|
请求方法 | GET |
请求地址 | /sms_codes/(?P<mobile>1[3-9]\d{9})/ |
2.请求参数:路径参数
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
mobile | string | 是 | 手机号 |
image_code | string | 是 | 图形验证码 |
uuid | string | 是 | 唯一编号 |
3.响应结果:JSON
字段 | 说明 |
---|---|
code | 状态码。0表示成功;400代表失败 |
errmsg | 错误信息 |
2. 短信验证码后端接口定义
class SMSCodeView(View):
"""短信验证码"""
def get(self, reqeust, mobile):
"""
:param reqeust: 请求对象
:param mobile: 手机号
:return: JSON
"""
pass
3. 短信验证码后端逻辑实现
from libs.yuntongxun.sms import CCP
from django import http
import random
import logging
logger = logging.getLogger('django')
class SMSCodeView(View):
"""短信验证码"""
def get(self, reqeust, mobile):
"""
:param reqeust: 请求对象
:param mobile: 手机号
:return: JSON
"""
# 接收参数
image_code_client = reqeust.GET.get('image_code')
uuid = reqeust.GET.get('image_code_id')
# 校验参数
if not all([image_code_client, uuid]):
return http.JsonResponse({'code': 400, 'errmsg': '缺少必传参数'})
# 创建连接到redis的对象
redis_conn = get_redis_connection('code')
# 提取图形验证码
image_code_server = redis_conn.get('img_%s' % uuid)
if image_code_server is None:
# 图形验证码过期或者不存在
return http.JsonResponse({'code': 400, 'errmsg': '图形验证码失效'})
# 删除图形验证码,避免恶意测试图形验证码
try:
redis_conn.delete('img_%s' % uuid)
except Exception as e:
logger.error(e)
# 对比图形验证码
image_code_server = image_code_server.decode() # bytes转字符串
if image_code_client.lower() != image_code_server.lower(): # 转小写后比较
return http.JsonResponse({'code': 400, 'errmsg': '输入图形验证码有误'})
# 生成短信验证码:生成6位数验证码
sms_code = '%06d' % random.randint(0, 999999)
logger.info(sms_code)
# 保存短信验证码
redis_conn.setex('sms_%s' % mobile, 300, sms_code)
# 发送短信验证码
CCP().send_template_sms(mobile,[sms_code, 5], 1)
# 响应结果
return http.JsonResponse({'code': 0, 'errmsg': '发送短信成功'})
2.4 补充注册时短信验证逻辑(作业)
1. 补充注册时短信验证后端逻辑
1.接收短信验证码参数
sms_code_client = request.POST.get('sms_code')
2.保存注册数据之前,对比短信验证码
# 判断短信验证码是否正确:跟图形验证码的验证一样的逻辑
# 提取服务端存储的短信验证码:以前怎么存储,现在就怎么提取
redis_conn = get_redis_connection('verify_code')
sms_code_server = redis_conn.get('sms_%s' % mobile) # sms_code_server是bytes
# 判断短信验证码是否过期
if not sms_code_server:
return http.JsonResponse({'code': 400, 'errmsg': '短信验证码失效'})
# 对比用户输入的和服务端存储的短信验证码是否一致
if sms_code != sms_code_server.decode():
return http.JsonResponse({'code': 400, 'errmsg': '短信验证码有误'})
2.5 避免频繁发送短信验证码
存在的问题:
- 虽然我们在前端界面做了60秒倒计时功能。
- 但是恶意用户可以绕过前端界面向后端频繁请求短信验证码。
解决办法:
- 在后端也要限制用户请求短信验证码的频率。60秒内只允许一次请求短信验证码。
- 在Redis数据库中缓存一个数值,有效期设置为60秒。
1. 避免频繁发送短信验证码逻辑分析
2. 避免频繁发送短信验证码逻辑实现
1.提取、校验send_flag
send_flag = redis_conn.get('send_flag_%s' % mobile)
if send_flag:
return http.JsonResponse({'code': 400, 'errmsg': '发送短信过于频繁'})
2.重新写入send_flag
# 保存短信验证码
redis_conn.setex('sms_%s' % mobile, 300, sms_code)
# 写入send_flag
redis_conn.setex('send_flag_%s' % mobile, 60, 1)
2.6 pipeline操作Redis数据库
Redis的 C - S 架构:
- 基于客户端-服务端模型以及请求/响应协议的TCP服务。
- 客户端向服务端发送一个查询请求,并监听Socket返回。
- 通常是以阻塞模式,等待服务端响应。
- 服务端处理命令,并将结果返回给客户端。
存在的问题:
- 如果Redis服务端需要同时处理多个请求,加上网络延迟,那么服务端利用率不高,效率降低。
解决的办法:
- 管道pipeline
1. pipeline的介绍
管道pipeline
- 可以一次性发送多条命令并在执行完后一次性将结果返回。
- pipeline通过减少客户端与Redis的通信次数来实现降低往返延时时间。
实现的原理
- 实现的原理是队列。
- Client可以将三个命令放到一个tcp报文一起发送。
- Server则可以将三条命令的处理结果放到一个tcp报文返回。
- 队列是先进先出,这样就保证数据的顺序性。
2. pipeline操作Redis数据库
1.实现步骤
1. 创建Redis管道
2. 将Redis请求添加到队列
3. 执行请求
2.代码实现
# 创建Redis管道
pl = redis_conn.pipeline()
# 将Redis请求添加到队列
pl.setex('sms_%s' % mobile, 300, sms_code)
pl.setex('send_flag_%s' % mobile, 60, 1)
# 执行请求
pl.execute()
三、异步方案Celery
3.1 消息队列介绍和使用
思考:
- 如何将
发送短信
从主业务中解耦
出来。
1. 生产者消费者设计模式
- 最常用的解耦方式之一,寻找中间人(broker)搭桥,保证两个业务没有直接关联。
- 我们称这一解耦方式为:生产者消费者设计模式
总结:
- 生产者生成消息,缓存到消息队列中,消费者读取消息队列中的消息并执行。
- 由美多商城生成发送短信消息,缓存到消息队列中,消费者读取消息队列中的发送短信消息并执行。
2.中间人broker
- 示例:此处演示Redis数据库作为中间人broker
- Celery需要一种解决消息的发送和接受的方式,我们把这种用来存储消息的的中间装置叫做message broker, 也可叫做消息中间人。
-
作为中间人,我们有几种方案可选择:
-
1.RabbitMQ
- RabbitMQ是一个功能完备,稳定的并且易于安装的broker. 它是生产环境中最优的选择。
-
使用RabbitMQ的细节参照以下链接:http://docs.celeryproject.org/en/latest/getting-started/brokers/rabbitmq.html#broker-rabbitmq
-
如果使用的是Ubuntu或者Debian发行版的Linux,可以直接通过命令安装RabbitMQ:
sudo apt-get install rabbitmq-server
-
安装完毕之后,RabbitMQ-server服务器就已经在后台运行。
-
如果用的并不是Ubuntu或Debian, 可以在以下网址:
Downloading and Installing RabbitMQ — RabbitMQ
去查找自己所需要的版本软件。
-
2.Redis
- Redis也是一款功能完备的broker可选项,但是其更可能因意外中断或者电源故障导致数据丢失的情况。
- 关于是由那个Redis作为Broker,可访下面网址: http://docs.celeryproject.org/en/latest/getting-started/brokers/redis.html#broker-redis
-
3.2 Celery介绍和使用
思考:
- 消费者取到消息之后,要消费掉(执行任务),需要我们去实现。
- 任务可能出现高并发的情况,需要补充多任务的方式执行。
- 耗时任务很多种,每种耗时任务编写的生产者和消费者代码有重复。
- 取到的消息什么时候执行,以什么样的方式执行。
结论:
- 实际开发中,我们可以借助成熟的工具
Celery
来完成。- 有了
Celery
,我们在使用生产者消费者模式时,只需要关注任务本身,极大的简化了程序员的开发流程。
1. Celery介绍
-
Celery介绍:
- 一个简单、灵活且可靠、处理大量消息的分布式系统,可以在一台或者多台机器上运行。
- Celery是一个功能完备即插即用的任务队列
- 单个 Celery 进程每分钟可处理数以百万计的任务。
- 通过消息进行通信,使用
消息队列(broker)
在客户端
和消费者
之间进行协调。
-
安装Celery:
$ pip install -U Celery
2. 创建Celery实例并加载配置
1.定义Celery包
2.创建Celery实例
celery_tasks.main.py
# celery启动文件
from celery import Celery
# 为celery使用django配置文件进行设置
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "meiduo_mall.settings")
# 创建celery实例
celery_app = Celery('celery_tasks')
3.加载Celery配置
celery_tasks.config.py
broker_url = "redis://127.0.0.1/15"
###########################说明#############################
# 如果使用别的作为中间人, 例如使用 rabbitmq
# 则 rabbitmq 配置如下:
broker_url= 'amqp://用户名:密码@ip地址:5672'
# 例如:
# meihao: 在rabbitq中创建的用户名, 注意: 远端链接时不能使用guest账户.
# 123456: 在rabbitq中用户名对应的密码
# ip部分: 指的是当前rabbitq所在的电脑ip
# 5672: 是规定的端口号
broker_url = 'amqp://meihao:123456@172.16.238.128:5672'
celery_tasks.main.py
# celery启动文件
from celery import Celery
# 创建celery实例
celery_app = Celery('meiduo')
# 加载celery配置
celery_app.config_from_object('celery_tasks.config')
3. 定义发送短信任务
1.注册任务:celery_tasks.main.py
# celery启动文件
from celery import Celery
# 创建celery实例
celery_app = Celery('celery_tasks')
# 加载celery配置
celery_app.config_from_object('celery_tasks.config')
# 自动注册celery任务
celery_app.autodiscover_tasks(['celery_tasks.sms'])
2.定义任务:celery_tasks.sms.tasks.py
from celery_tasks.main import celery_app
from libs.yuntongxun.sms import CCP
import logging
logger = logging.getLogger('django')
# name:异步任务别名
@celery_app.task(name='send_sms_code')
def send_sms_code(mobile, sms_code):
"""
发送短信异步任务
:param mobile: 手机号
:param sms_code: 短信验证码
"""
try:
send_ret = CCP().send_template_sms(mobile, [sms_code, 5], 1)
except Exception as e:
logger.error(e)
4. 启动Celery服务
$ cd ~/meiduo_project/meiduo_mall
$ celery -A celery_tasks.main worker -l info
-A
指对应的应用程序, 其参数是项目中 Celery实例的位置。worker
指这里要启动的worker。-l
指日志等级,比如info
等级。
5. 调用发送短信任务
# 发送短信验证码
# CCP().send_template_sms(mobile,[sms_code, 5], 1)
# Celery异步发送短信验证码
send_sms_code.delay(mobile, sms_code)
6. 补充celery worker的工作模式
- 默认是进程池方式,进程数以当前机器的CPU核数为参考,每个CPU开四个进程。
- 如何自己指定进程数:
celery worker -A proj --concurrency=4
- 如何改变进程池方式为协程方式:
celery worker -A proj --concurrency=1000 -P eventlet -c 1000
# 安装eventlet模块
$ pip install eventlet
# 启用 Eventlet 池
$ celery -A celery_tasks.main worker -l info -P eventlet -c 1000