用户登录、登出、注册功能的实现
## 用户注册功能
[TOC]
### 一、设计接口思路
- 分析业务逻辑,明确在这个业务中需要涉及到几个相关子业务,将每个子业务当做一个接口来设计
- 分析接口的功能任务,明确接口的访问方式与返回数据:
- 接口的请求方式,如GET 、POST 、PUT等
- 接口的URL路径定义
- 需要前端传递的数据及数据格式(如路径参数、查询字符串、请求体表单、JSON等)
- 返回给前端的数据及数据格式
### 二、功能分析
- 用户名判断是否存在
- 手机号判断是否存在
- 图片验证码
- 短信验证码
- 注册保存用户数据
图片验证码、短信验证码考虑到后续可能会在其他业务中也会用到,因此将验证码功能独立出来,**创建一个新应用verifications,在此应用中实现图片验证码、短信验证码**
### 三、图片验证码接口代码实现
#### 1.分析
**请求方法**:**GET**
**url定义**:`/image_codes/<uuid:image_code_id>/`
**请求参数**:url路径参数
| 参数 | 类型 | 前端是否必须传 | 描述 |
| ------------- | ---------- | -------------- | -------------- |
| image_code_id | uuid字符串 | 是 | 图片验证码编号 |
uuid:Universally unique identifier(eg. 123e4567-e89b-12d3-a456-426655440000)
#### 2.后端代码实现
a.将生成图像验证码的模块文件夹(百度云盘有提供captcha文件夹)复制粘贴到项目根目录utils文件夹下
b.由于验证(图片验证、短信验证)功能,以后有可能在其他应用或项目中重用,所以单独创建一个应用来实现,所有验证相关的业务逻辑接口。在apps目录中创建一个verifications应用,并在settings.py文件中的INSTALLED_APPS列表中指定。
```python
# 在verifications/views.py文件中添加如下代码:
import logging
from django.shortcuts import render
from django.views import View
from django_redis import get_redis_connection
from django.http import HttpResponse
from utils.captcha.captcha import captcha
# 安装图片验证码所需要的 Pillow 模块
# pip install Pillow
from . import constants
from users.models import Users
# 导入日志器
logger = logging.getLogger('django')
class ImageCode(View):
"""
define image verification view
# /image_codes/<uuid:image_code_id>/
"""
def get(self, request, image_code_id):
text, image = captcha.generate_captcha()
# 确保settings.py文件中有配置redis CACHE
# Redis原生指令参考 http://redisdoc.com/index.html
# Redis python客户端 方法参考 http://redis-py.readthedocs.io/en/latest/#indices-and-tables
con_redis = get_redis_connection(alias='verify_codes')
img_key = "img_{}".format(image_code_id).encode('utf-8')
# 将图片验证码的key和验证码文本保存到redis中,并设置过期时间
con_redis.setex(img_key, constants.IMAGE_CODE_REDIS_EXPIRES, text)
logger.info("Image code: {}".format(text))
return HttpResponse(content=image, content_type="images/jpg")
```
c.为了保存应用中用到的常量信息,需要在verifications应用下创建一个constants.py文件
```python
# 在verifications/constants.py文件中加入如下代码:
# 图片验证码redis有效期,单位秒
IMAGE_CODE_REDIS_EXPIRES = 5 * 60
```
d.本项目需要将图形验证码、短信验证码以及用户的会话信息保存到redis服务器中,所以需要在settings.py文件中指定如下配置信息:
```python
# settings.py文件中加入如下内容:
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache", # 指定redis缓存后端
"LOCATION": "redis://127.0.0.1:6379/0",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
# "PASSWORD": "mysecret"
}
},
# 同样可以指定多个redis
"session": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
},
"verify_codes": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/2",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
},
"sms_codes": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/3",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
},
}
# 将用户的session保存到redis中
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
# 指定缓存redis的别名
SESSION_CACHE_ALIAS = "session"
```
e.在verifications应用下创建一个urls.py文件并添加如下内容:
```python
# verifications应用下创建一个urls.py
from django.urls import path, re_path
from . import views
app_name = "verifications"
urlpatterns = [
# re_path(r'^image_codes/(?P<image_code_id>[\w-]+)/$', view=views.ImageCodeView.as_view(), name="image_code"),
# image_code_id为uuid格式
path('image_codes/<uuid:image_code_id>/', views.ImageCode.as_view(), name='image_code'),
]
```
#### 3.前端代码实现
html代码:
```jinja2
{# 继承base基类模版 #}
{% extends 'base/base.html' %}
{% block link %}
{# <link rel="stylesheet" href="../../static/css/users/auth.css">#}
<link rel="stylesheet" href="{% static 'css/users/auth.css' %}">
{% endblock %}
{% block title %}
注册
{% endblock %}
<!-- main-contain start -->
{% block main_start %}
<main id="container">
<div class="register-contain">
<div class="top-contain">
<h4 class="please-register">请注册</h4>
<a href="javascript:void(0);" class="login">立即登录 ></a>
</div>
<form action="" method="post" class="form-contain">
<div class="form-item">
<input type="text" placeholder="请输入用户名" name="username" class="form-control" autocomplete="off">
</div>
<div class="form-item">
<input type="password" placeholder="请输入密码" name="password" class="form-control">
</div>
<div class="form-item">
<input type="password" placeholder="请输入确认密码" name="password_repeat" class="form-control">
</div>
<div class="form-item">
<input type="tel" placeholder="请输入手机号" name="telephone" class="form-control" autocomplete="off" autofocus>
</div>
<div class="form-item">
<input type="text" placeholder="请输入图形验证码" name="captcha_graph" class="form-captcha">
<a href="javascript:void(0);" class="captcha-graph-img">
<img src="" alt="验证码" title="点击刷新" >
</a>
</div>
<div class="form-item">
<input type="text" placeholder="请输入短信验证码" name="sms_captcha" class="form-captcha" autocomplete="off">
<a href="javascript:void(0);" class="sms-captcha" title="发送验证码">获取短信验证码</a>
</div>
<div class="form-item">
<input type="submit" value="立即注册" class="register-btn">
</div>
</form>
</div>
</main>
{% endblock %}
<!-- main-contain end -->
{% block hot_recommend %}
{% endblock %}
{% block script %}
{# <script src="/static/js/users/auth.js"></script>#}
<script src="{% static 'js/users/auth.js' %}"></script>
{% endblock %}
```
在js文件夹下创建一个users文件夹用户存放用户模块相关的js文件,在users文件下创建auth.js文件。
```javascript
$(function () {
let $img = $(".form-item .captcha-graph-img img"); // 获取图像标签
let sImageCodeId = ""; // 定义图像验证码ID值
generateImageCode(); // 生成图像验证码图片
$img.click(generateImageCode); // 点击图片验证码生成新的图片验证码图片
// 生成一个图片验证码的编号,并设置页面中图片验证码img标签的src属性
function generateImageCode() {
// 1、生成一个图片验证码随机编号
sImageCodeId = generateUUID();
// 2、拼接请求url /image_codes/<uuid:image_code_id>/
let imageCodeUrl = "/image_codes/" + sImageCodeId + "/";
// 3、修改验证码图片src地址
$img.attr('src', imageCodeUrl)
}
// 生成图片UUID验证码
function generateUUID() {
let d = new Date().getTime();
if (window.performance && typeof window.performance.now === "function") {
d += performance.now(); //use high-precision timer if available
}
let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
}
});
```
### 四、判断用户名是否注册功能实现
#### 1.分析
**请求方法**:**GET**
**url定义**:`/usernames/(?P<username>\w{5,20})/`
**请求参数**:url路径参数
| 参数 | 类型 | 前端是否必须传 | 描述 |
| -------- | ------ | -------------- | ---------------- |
| username | 字符串 | 是 | 用户输入的用户名 |
#### 2.后端代码实现
```python
from utils.json_fun import to_json_data
from django.views import View
class CheckUsernameView(View):
"""
Check whether the user exists
GET usernames/(?P<username>\w{5,20})/
"""
def get(self, request, username):
# count = 1 if User.objects.get(username=username) else 0
data = {
'username': username,
'count': Users.objects.filter(username=username).count()
}
return to_json_data(data=data)
```
在项目根目录中的utils目录下创建json_fun.py文件,用于处理json格式转化功能。
```python
from django.http import JsonResponse
from .res_code import Code
def to_json_data(errno=Code.OK, errmsg='', data=None, kwargs=None):
json_dict = {'errno': errno, 'errmsg': errmsg, 'data': data}
if kwargs and isinstance(kwargs, dict) and kwargs.keys():
json_dict.update(kwargs)
return JsonResponse(json_dict)
```
在项目根目录中的utils目录下创建res_code.py文件,用于把后端执行的情况返回给前端。
```python
class Code:
OK = "0"
DBERR = "4001"
NODATA = "4002"
DATAEXIST = "4003"
DATAERR = "4004"
METHERR = "4005"
SMSERROR = "4006"
SMSFAIL = "4007"
SESSIONERR = "4101"
LOGINERR = "4102"
PARAMERR = "4103"
USERERR = "4104"
ROLEERR = "4105"
PWDERR = "4106"
SERVERERR = "4500"
UNKOWNERR = "4501"
error_map = {
Code.OK : "成功",
Code.DBERR : "数据库查询错误",
Code.NODATA : "无数据",
Code.DATAEXIST : "数据已存在",
Code.DATAERR : "数据错误",
Code.METHERR : "方法错误",
Code.SMSERROR : "发送短信验证码异常",
Code.SMSFAIL : "发送短信验证码失败",
Code.SESSIONERR : "用户未登录",
Code.LOGINERR : "用户登录失败",
Code.PARAMERR : "参数错误",
Code.USERERR : "用户不存在或未激活",
Code.ROLEERR : "用户身份错误",
Code.PWDERR : "密码错误",
Code.SERVERERR : "内部错误",
Code.UNKOWNERR : "未知错误",
}
```
```python
# url 定义
from django.urls import path, re_path
from . import views
app_name = "verifications"
urlpatterns = [
# image_code_id为uuid格式
path('image_codes/<uuid:image_code_id>/', views.ImageCode.as_view(), name='image_code'),
re_path('usernames/(?P<username>\w{5,20})/', views.CheckUsernameView.as_view(), name='check_username'),
]
```
#### 3.前端代码实现
```javascript
$(function () {
let $username = $('#user_name');
let $img = $(".form-item .captcha-graph-img img");
let sImageCodeId = "";
// 1、图像验证码逻辑
generateImageCode(); // 生成图像验证码图片
$img.click(generateImageCode); // 点击图片验证码生成新的图片验证码图片
// 2、用户名验证逻辑
$username.blur(function () {
fn_check_usrname();
});
// 生成一个图片验证码的编号,并设置页面中图片验证码img标签的src属性
function generateImageCode() {
// 1、生成一个图片验证码随机编号
sImageCodeId = generateUUID();
// 2、拼接请求url /image_codes/<uuid:image_code_id>/
let imageCodeUrl = "/image_codes/" + sImageCodeId + "/";
// 3、修改验证码图片src地址
$img.attr('src', imageCodeUrl)
}
// 生成图片UUID验证码
function generateUUID() {
let d = new Date().getTime();
if (window.performance && typeof window.performance.now === "function") {
d += performance.now(); //use high-precision timer if available
}
let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
}
// 判断用户名是否已经注册
function fn_check_usrname() {
let sUsername = $username.val(); // 获取用户名字符串
if (sUsername === "") {
message.showError('用户名不能为空!');
return
}
if (!(/^\w{5,20}$/).test(sUsername)) {
message.showError('请输入5-20个字符的用户名');
return
}
// 发送ajax请求,去后端查询用户名是否存在
$.ajax({
url: '/usernames/' + sUsername + '/',
type: 'GET',
dataType: 'json',
})
.done(function (res) {
if (res.data.count !== 0) {
message.showError(res.data.username + '已注册,请重新输入!')
} else {
message.showInfo(res.data.username + '能正常使用!')
}
})
.fail(function () {
message.showError('服务器超时,请重试!');
});
}
});
```
### 五、判断手机号是否注册功能实现
#### 1.分析
**请求方法**:**GET**
**url定义**:`/mobiles/(?P<mobile>1[3-9]\d{9})/`
**请求参数**:url路径参数
| 参数 | 类型 | 前端是否必须传 | 描述 |
| ------ | ------ | -------------- | ---------------- |
| mobile | 字符串 | 是 | 用户输入的手机号 |
#### 2.后端代码实现
```python
# 在verifications目录下的views.py文件中定义如下类视图:
class CheckMobileView(View):
"""
Check whether the mobile exists
GET mobiles/(?P<mobile>1[3-9]\d{9})/
"""
def get(self, request, mobile):
data = {
'mobile': mobile,
'count': Users.objects.filter(mobile=mobile).count()
}
return to_json_data(data=data)
```
```python
# 在verifications目录下的urls.py文件中定义如下路由:
from django.urls import path, re_path
from . import views
app_name = "verifications"
urlpatterns = [
re_path('mobiles/(?P<mobile>1[3-9]\d{9})/', views.CheckMobileView.as_view(), name='check_mobiles'),
]
```
#### 3.前端代码实现
```python
$(function () {
let $username = $('#user_name'); // 选择id为user_name的网页元素,需要定义一个id为user_name
let $img = $(".form-item .captcha-graph-img img"); // 获取图像标签
let sImageCodeId = ""; // 定义图像验证码ID值
let $mobile = $('#mobile'); // 选择id为mobile的网页元素,需要定义一个id为mobile
// 1、图像验证码逻辑
generateImageCode(); // 生成图像验证码图片
$img.click(generateImageCode); // 点击图片验证码生成新的图片验证码图片
// 判断用户是否注册
// 2、用户名验证逻辑
$username.blur(function () {
fn_check_usrname();
});
// 3、手机号验证逻辑
// 判断用户手机号是否注册
$mobile.blur(function () {
fn_check_mobile();
});
// 生成一个图片验证码的编号,并设置页面中图片验证码img标签的src属性
function generateImageCode() {
// 1、生成一个图片验证码随机编号
sImageCodeId = generateUUID();
// 2、拼接请求url /image_codes/<uuid:image_code_id>/
let imageCodeUrl = "/image_codes/" + sImageCodeId + "/";
// 3、修改验证码图片src地址
$img.attr('src', imageCodeUrl)
}
// 生成图片UUID验证码
function generateUUID() {
let d = new Date().getTime();
if (window.performance && typeof window.performance.now === "function") {
d += performance.now(); //use high-precision timer if available
}
let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
}
// 判断用户名是否已经注册
function fn_check_usrname() {
let sUsername = $username.val(); // 获取用户名字符串
if (sUsername === "") {
message.showError('用户名不能为空!');
return
}
// test()方法 判断字符串中是否匹配到正则表达式内容,返回的是boolean值 ( true / false )
if (!(/^\w{5,20}$/).test(sUsername)) {
message.showError('请输入5-20个字符的用户名');
return
}
// 发送ajax请求,去后端查询用户名是否存在
$.ajax({
url: '/usernames/' + sUsername + '/',
type: 'GET',
dataType: 'json',
// data:{'code':300268}
})
.done(function (res) {
if (res.data.count !== 0) {
message.showError(res.data.username + '已注册,请重新输入!')
} else {
message.showInfo(res.data.username + '能正常使用!')
}
})
.fail(function () {
message.showError('服务器超时,请重试!');
});
}
function fn_check_mobile() {
let sMobile = $mobile.val(); // 获取用户输入的手机号码字符串
let SreturnValue = "";
if (sMobile === "") {
message.showError('手机号不能为空!');
return
}
if (!(/^1[345789]\d{9}$/).test(sMobile)) {
message.showError('手机号码格式不正确,请重新输入!');
return
}
$.ajax({
url: '/mobiles/' + sMobile + '/',
type: 'GET',
dataType: 'json',
async: false // 把async关掉
})
.done(function (res) {
if (res.data.count !== 0) {
message.showError(res.data.mobile + '已注册,请重新输入!')
SreturnValue = ""
} else {
SreturnValue = "success"
}
})
.fail(function () {
message.showError('服务器超时,请重试!');
SreturnValue = ""
});
return SreturnValue
}
});
```
### 六、发送手机短信验证码功能实现
#### 1.分析
业务处理流程:
- 判断图片验证码是否正确
- 判断是否在60s内有发送记录
- 生成短信验证码
- 保存短信验证码与发送记录
- 发送短信
**请求方法**:**POST**
**url定义**:`/sms_codes/`
**请求参数**:url路径参数
| 参数 | 类型 | 前端是否必须传 | 描述 |
| ------------- | ------ | -------------- | ------------------------ |
| mobile | 字符串 | 是 | 用户输入的手机号 |
| image_code_id | UUID | 是 | js生成的图片uuid号 |
| text | 字符串 | 是 | 用户输入的图片验证码文本 |
注:由于是post请求,在向后端发起请求时,需要附带csrf token
#### 2.后端代码实现
```python
# 在verifications目录下的views.py文件中定义如下类视图:
import logging
import json
import random
import string
from django.views import View
from django_redis import get_redis_connection
from . import constants
from utils.json_fun import to_json_data
from utils.res_code import Code, error_map
from users.models import Users
from . import forms
from utils.yuntongxun.sms import CCP
# 导入日志器
logger = logging.getLogger('django')
class SmsCodesView(View):
"""
send mobile sms code
POST /sms_codes/
"""
def post(self, request):
# 1、
json_data = request.body
if not json_data:
return to_json_data(errno=Code.PARAMERR, errmsg=error_map[Code.PARAMERR])
# 将json转化为dict
dict_data = json.loads(json_data.decode('utf8'))
# 2、
form = forms.CheckImgCodeForm(data=dict_data)
if form.is_valid():
# 获取手机号
mobile = form.cleaned_data.get('mobile')
# 3、
# 创建短信验证码内容
sms_num = ''.join([random.choice(string.digits) for _ in range(constants.SMS_CODE_NUMS)])
# 将短信验证码保存到数据库
# 确保settings.py文件中有配置redis CACHE
# Redis原生指令参考 http://redisdoc.com/index.html
# Redis python客户端 方法参考 http://redis-py.readthedocs.io/en/latest/#indices-and-tables
# 4、
redis_conn = get_redis_connection(alias='verify_codes')
pl = redis_conn.pipeline()
# 创建一个在60s以内是否有发送短信记录的标记
sms_flag_fmt = "sms_flag_{}".format(mobile)
# 创建保存短信验证码的标记key
sms_text_fmt = "sms_{}".format(mobile)
# 此处设置为True会出现bug
try:
pl.setex(sms_flag_fmt.encode('utf8'), constants.SEND_SMS_CODE_INTERVAL, 1)
pl.setex(sms_text_fmt.encode('utf8'), constants.SMS_CODE_REDIS_EXPIRES, sms_num)
# 让管道通知redis执行命令
pl.execute()
except Exception as e:
logger.debug("redis 执行出现异常:{}".format(e))
return to_json_data(errno=Code.UNKOWNERR, errmsg=error_map[Code.UNKOWNERR])
logger.info("Sms code: {}".format(sms_num))
# 发送短语验证码
try:
result = CCP().send_template_sms(mobile,
[sms_num, constants.SMS_CODE_YUNTX_EXPIRES],
constants.SMS_CODE_TEMP_ID)
except Exception as e:
logger.error("发送验证码短信[异常][ mobile: %s, message: %s ]" % (mobile, e))
return to_json_data(errno=Code.SMSERROR, errmsg=error_map[Code.SMSERROR])
else:
if result == 0:
logger.info("发送验证码短信[正常][ mobile: %s sms_code: %s]" % (mobile, sms_num))
return to_json_data(errno=Code.OK, errmsg="短信验证码发送成功")
else:
logger.warning("发送验证码短信[失败][ mobile: %s ]" % mobile)
return to_json_data(errno=Code.SMSFAIL, errmsg=error_map[Code.SMSFAIL])
else:
# 定义一个错误信息列表
err_msg_list = []
for item in form.errors.get_json_data().values():
err_msg_list.append(item[0].get('message'))
# print(item[0].get('message')) # for test
err_msg_str = '/'.join(err_msg_list) # 拼接错误信息为一个字符串
return to_json_data(errno=Code.PARAMERR, errmsg=err_msg_str)
```
```python
# 在verifications目录下的forms.py文件中定义如下form表单:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
-------------------------------------------------
@Time : 2018/12/3 4:18 PM
@Auth : Youkou
@Site : www.youkou.site
@File : forms.py
@IDE : PyCharm
@Edit : 2018/12/3
-------------------------------------------------
"""
from django import forms
from django.core.validators import RegexValidator
from django_redis import get_redis_connection
from users.models import Users
# 创建手机号的正则校验器
mobile_validator = RegexValidator(r"^1[3-9]\d{9}$", "手机号码格式不正确")
class CheckImgCodeForm(forms.Form):
"""
check image code
"""
mobile = forms.CharField(max_length=11, min_length=11, validators=[mobile_validator, ],
error_messages={"min_length": "手机号长度有误", "max_length": "手机号长度有误",
"required": "手机号不能为空"})
image_code_id = forms.UUIDField(error_messages={"required": "图片UUID不能为空"})
text = forms.CharField(max_length=4, min_length=4,
error_messages={"min_length": "图片验证码长度有误", "max_length": "图片验证码长度有误",
"required": "图片验证码不能为空"})
# Cleaning and validating fields that depend on each other
def clean(self):
cleaned_data = super().clean()
# 1、
image_uuid = cleaned_data.get("image_code_id")
image_text = cleaned_data.get("text")
mobile_num = cleaned_data.get("mobile")
# 2、
if Users.objects.filter(mobile=mobile_num).count():
raise forms.ValidationError("手机号已注册,请重新输入")
# 确保settings.py文件中有配置redis CACHE
# Redis原生指令参考 http://redisdoc.com/index.html
# Redis python客户端 方法参考 http://redis-py.readthedocs.io/en/latest/#indices-and-tables
# 2、
con_redis = get_redis_connection(alias='verify_codes')
# 创建保存到redis中图片验证码的key
img_key = "img_{}".format(image_uuid).encode('utf-8')
# 取出图片验证码
real_image_code_origin = con_redis.get(img_key)
real_image_code = real_image_code_origin.decode('utf-8') if real_image_code_origin else None
con_redis.delete(img_key)
# 验证手机号
if (not real_image_code) or (image_text != real_image_code):
raise forms.ValidationError("图片验证失败")
# 检查是否在60s内有发送记录
sms_flag_fmt = "sms_flag_{}".format(mobile_num).encode('utf-8')
sms_flag = con_redis.get(sms_flag_fmt)
if sms_flag:
raise forms.ValidationError("获取手机短信验证码过于频繁")
```
```python
# 在verifications目录下的constants.py文件中定义如下常数:
# 图片验证码redis有效期,单位秒
IMAGE_CODE_REDIS_EXPIRES = 5 * 60
# 短信验证码有效期,单位秒
SMS_CODE_REDIS_EXPIRES = 5 * 60
# 云通讯短信验证码过期时间(发送短信是显示为5分钟)
SMS_CODE_YUNTX_EXPIRES = 5
# 发送间隔
SEND_SMS_CODE_INTERVAL = 60
# 短信发送模板
SMS_CODE_TEMP_ID = 1
# 短信验证码位数
SMS_CODE_NUMS = 6
```
#### 3. 短信验证码平台-云通讯
a.**本项目中使用的短信发送模块为**[云通讯](<http://www.yuntongxun.com/>)平台:
- 用户发送短信(或语音)验证码的第三方平台
- [参考文档地址](<http://www.yuntongxun.com/doc/ready/demo/1_4_1_2.html>)
b.**注册登录**
- 免费注册登录
- 赠送8元,用于测试
- 身份认证之后,才能正常使用
c.**获取开发者相关参数**
![yuntongxun_1](../images/yuntongxun_1.jpg)
```python
_accountSid = '开发者主账号中的ACCOUNT SID'
# 说明:主账号Token,登陆云通讯网站后,可在控制台-应用中看到开发者主账号AUTH TOKEN
_accountToken = '开发者主账号中的AUTH TOKEN'
# 请使用管理控制台首页的APPID或自己创建应用的APPID
_appId = '开发者主账号中的AppID(默认)'
# 说明:请求地址,生产环境配置成app.cloopen.com
_serverIP = 'sandboxapp.cloopen.com'
# 说明:请求端口 ,生产环境为8883
_serverPort = "8883"
# 说明:REST API版本号保持不变
_softVersion = '2013-12-26'
```
![modify_parm](../images/modify_parms.jpg)
d.**设置测试账号**
![edit_test_mobile](../images/yuntongxun_2.jpg)
#### 4.前端代码实现
```javascript
$(function () {
let $username = $('#user_name'); // 选择id为user_name的网页元素,需要定义一个id为user_name
let $img = $(".form-item .captcha-graph-img img"); // 获取图像标签
let sImageCodeId = ""; // 定义图像验证码ID值
let $mobile = $('#mobile'); // 选择id为mobile的网页元素,需要定义一个id为mobile
let $smsCodeBtn = $('.form-item .sms-captcha'); // 获取短信验证码按钮元素,需要定义一个id为input_smscode
let $imgCodeText = $('#input_captcha'); // 获取用户输入的图片验证码元素,需要定义一个id为input_captcha
// 1、图像验证码逻辑
generateImageCode(); // 生成图像验证码图片
$img.click(generateImageCode); // 点击图片验证码生成新的图片验证码图片
// 2、判断用户名是否注册
$username.blur(function () {
fn_check_usrname();
});
// 3、判断用户手机号是否注册
$mobile.blur(function () {
fn_check_mobile();
});
// 4、发送短信逻辑
$smsCodeBtn.click(function () {
// 判断手机号是否输入
if (fn_check_mobile() !== "success") {
return
}
// 判断用户是否输入图片验证码
let text = $imgCodeText.val(); // 获取用户输入的图片验证码文本
if (!text) {
message.showError('请填写验证码!');
return
}
// 判断是否生成的UUID
if (!sImageCodeId) {
message.showError('图片UUID为空');
return
}
// 正常获取参数
let SdataParams = {
"mobile": $mobile.val(), // 获取用户输入的手机号
"text": text, // 获取用户输入的图片验证码文本
"image_code_id": sImageCodeId // 获取图片UUID
};
// for test
// let SdataParams = {
// "mobile": "1886608", // 获取用户输入的手机号
// "text": "ha3d", // 获取用户输入的图片验证码文本
// "image_code_id": "680a5a66-d9e5-4c3c-b8ea" // 获取图片UUID
// };
// 向后端发送请求
$.ajax({
// 请求地址
url: "/sms_codes/",
// 请求方式
type: "POST",
data: JSON.stringify(SdataParams),
// 请求内容的数据类型(前端发给后端的格式)
contentType: "application/json; charset=utf-8",
// 响应数据的格式(后端返回给前端的格式)
dataType: "json",
async: false // 关掉异步功能
})
.done(function (res) {
if (res.errno === "0") {
// 倒计时60秒,60秒后允许用户再次点击发送短信验证码的按钮
message.showSuccess('短信验证码发送成功');
let num = 60;
// 设置一个计时器
let t = setInterval(function () {
if (num === 1) {
// 如果计时器到最后, 清除计时器对象
clearInterval(t);
// 将点击获取验证码的按钮展示的文本恢复成原始文本
$smsCodeBtn.html("获取验证码");
} else {
num -= 1;
// 展示倒计时信息
$smsCodeBtn.html(num + "秒");
}
}, 1000);
} else {
message.showError(res.errmsg);
}
})
.fail(function(){
message.showError('服务器超时,请重试!');
});
});
// 生成一个图片验证码的编号,并设置页面中图片验证码img标签的src属性
function generateImageCode() {
// 1、生成一个图片验证码随机编号
sImageCodeId = generateUUID();
// 2、拼接请求url /image_codes/<uuid:image_code_id>/
let imageCodeUrl = "/image_codes/" + sImageCodeId + "/";
// 3、修改验证码图片src地址
$img.attr('src', imageCodeUrl)
}
// 生成图片UUID验证码
function generateUUID() {
let d = new Date().getTime();
if (window.performance && typeof window.performance.now === "function") {
d += performance.now(); //use high-precision timer if available
}
let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
}
// 判断用户名是否已经注册
function fn_check_usrname() {
let sUsername = $username.val(); // 获取用户名字符串
let sReturnValue = "";
if (sUsername === "") {
message.showError('用户名不能为空!');
return
}
// test()方法 判断字符串中是否匹配到正则表达式内容,返回的是boolean值 ( true / false )
if (!(/^\w{5,20}$/).test(sUsername)) {
message.showError('请输入5-20个字符的用户名');
return
}
// 发送ajax请求,去后端查询用户名是否存在
$.ajax({
url: '/usernames/' + sUsername + '/',
type: 'GET',
dataType: 'json',
async: false
})
.done(function (res) {
if (res.data.count !== 0) {
message.showError(res.data.username + '已注册,请重新输入!');
sReturnValue = ""
} else {
message.showInfo(res.data.username + '能正常使用!');
sReturnValue = ""
}
})
.fail(function () {
message.showError('服务器超时,请重试!');
sReturnValue = ""
});
return sReturnValue
}
function fn_check_mobile() {
let sMobile = $mobile.val(); // 获取用户输入的手机号码字符串
let sReturnValue = "";
if (sMobile === "") {
message.showError('手机号不能为空!');
return
}
if (!(/^1[345789]\d{9}$/).test(sMobile)) {
message.showError('手机号码格式不正确,请重新输入!');
return
}
$.ajax({
url: '/mobiles/' + sMobile + '/',
type: 'GET',
dataType: 'json',
async: false
})
.done(function (res) {
if (res.data.count !== 0) {
message.showError(res.data.mobile + '已注册,请重新输入!')
sReturnValue = ""
} else {
SreturnValue = "success"
}
})
.fail(function () {
message.showError('服务器超时,请重试!');
sReturnValue = ""
});
return sReturnValue
}
// get cookie using jQuery
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
let cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
let cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
// Setting the token on the AJAX request
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
}
}
});
});
```
### 七、用户注册功能实现
#### 1.分析
业务处理流程:
- 判断用户名是否为空,是否已注册
- 判断手机号是否为空,是否已注册
- 判断密码是否为空,格式是否正确
- 判断确认密码与密码是否相同
- 判断短信验证码是否为空,是否格式正确,是否与真实的短信验证码相同
**请求方法**:**POST**
**url定义**:`/users/register/`
**请求参数**:url路径参数
| 参数 | 类型 | 前端是否必须传 | 描述 |
| --------------- | ------ | -------------- | -------------------- |
| username | 字符串 | 是 | 用户输入的用户名 |
| password | 字符串 | 是 | 用户输入的密码 |
| password_repeat | 字符串 | 是 | 用户输入的重复密码 |
| mobile | 字符串 | 是 | 用户输入的手机号 |
| sms_code | 字符串 | 是 | 用户输入的短信验证码 |
注:由于是post请求,在向后端发起请求时,需要附带csrf token
#### 2.后端代码实现
```python
# 在users目录下的views.py文件中定义如下类视图:
import json
from django.shortcuts import render
from django.views import View
from .forms import RegisterForm
from .models import Users
from utils.json_fun import to_json_data
from utils.res_code import Code, error_map
class RegisterView(View):
"""
"""
def get(self, request):
"""
"""
return render(request, 'users/register.html')
def post(self, request):
"""
"""
# 1、
json_data = request.body
if not json_data:
return to_json_data(errno=Code.PARAMERR, errmsg=error_map[Code.PARAMERR])
# 将json转化为dict
dict_data = json.loads(json_data.decode('utf8'))
form = RegisterForm(data=dict_data)
if form.is_valid():
username = form.cleaned_data.get('username')
password = form.cleaned_data.get('password')
mobile = form.cleaned_data.get('mobile')
user = Users.objects.create_user(username=username, password=password, mobile=mobile)
login(request, user)
return to_json_data(errmsg="恭喜您,注册成功!")
else:
# 定义一个错误信息列表
err_msg_list = []
for item in form.errors.get_json_data().values():
err_msg_list.append(item[0].get('message'))
err_msg_str = '/'.join(err_msg_list)
return to_json_data(errno=Code.PARAMERR, errmsg=err_msg_str)
```
```python
# 在users目录下的forms.py文件中定义如下form表单:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
-------------------------------------------------
@Time : 2018/12/8 2:22 PM
@Auth : Youkou
@Site : www.youkou.site
@File : forms.py
@IDE : PyCharm
@Edit : 2018/12/8
-------------------------------------------------
"""
import re
from django import forms
from django_redis import get_redis_connection
from verifications.constants import SMS_CODE_NUMS
from .models import Users
class RegisterForm(forms.Form):
"""
"""
username = forms.CharField(label='用户名', max_length=20, min_length=5,
error_messages={"min_length": "用户名长度要大于5", "max_length": "用户名长度要小于20",
"required": "用户名不能为空"}
)
password = forms.CharField(label='密码', max_length=20, min_length=6,
error_messages={"min_length": "密码长度要大于6", "max_length": "密码长度要小于20",
"required": "密码不能为空"}
)
password_repeat = forms.CharField(label='确认密码', max_length=20, min_length=6,
error_messages={"min_length": "密码长度要大于6", "max_length": "密码长度要小于20",
"required": "密码不能为空"}
)
mobile = forms.CharField(label='手机号', max_length=11, min_length=11,
error_messages={"min_length": "手机号长度有误", "max_length": "手机号长度有误",
"required": "手机号不能为空"})
sms_code = forms.CharField(label='短信验证码', max_length=SMS_CODE_NUMS, min_length=SMS_CODE_NUMS,
error_messages={"min_length": "短信验证码长度有误", "max_length": "短信验证码长度有误",
"required": "短信验证码不能为空"})
def clean_mobile(self):
"""
"""
tel = self.cleaned_data.get('mobile')
if not re.match(r"^1[3-9]\d{9}$", tel):
raise forms.ValidationError("手机号码格式不正确")
if Users.objects.filter(mobile=tel).exists():
raise forms.ValidationError("手机号已注册,请重新输入!")
return tel
def clean(self):
"""
"""
#
cleaned_data = super().clean()
passwd = cleaned_data.get('password')
passwd_repeat = cleaned_data.get('password_repeat')
if passwd != passwd_repeat:
#
raise forms.ValidationError("两次密码不一致")
#
tel = cleaned_data.get('mobile')
sms_text = cleaned_data.get('sms_code')
# 建立redis连接
redis_conn = get_redis_connection(alias='verify_codes')
#
sms_fmt = "sms_{}".format(tel).encode('utf-8')
#
real_sms = redis_conn.get(sms_fmt)
#
if (not real_sms) or (sms_text != real_sms.decode('utf-8')):
raise forms.ValidationError("短信验证码错误")
```
```python
# 在users目录下的urls.py文件中添如下路由:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
-------------------------------------------------
@Time : 2018/11/29 9:58 AM
@Auth : Youkou
@Site : www.youkou.site
@File : urls.py
@IDE : PyCharm
@Edit : 2018/11/29
-------------------------------------------------
"""
from django.urls import path
from . import views
app_name = 'users'
urlpatterns = [
# path('register/', views.register, name='register'),
path('register/', views.RegisterView.as_view(), name='register'),
]
```
#### 3.前端代码实现
```javascript
$(function () {
let $username = $('#user_name'); // 选择id为user_name的网页元素,需要定义一个id为user_name
let $img = $(".form-item .captcha-graph-img img"); // 获取图像标签
let sImageCodeId = ""; // 定义图像验证码ID值
let $mobile = $('#mobile'); // 选择id为mobile的网页元素,需要定义一个id为mobile
let $smsCodeBtn = $('.form-item .sms-captcha'); // 获取短信验证码按钮元素,需要定义一个id为input_smscode
let $imgCodeText = $('#input_captcha'); // 获取用户输入的图片验证码元素,需要定义一个id为input_captcha
let $register = $('.form-contain'); // 获取注册表单元素
// 1、图片验证码逻辑
// 通过uuid生成验证码编号
// 拼接验证码地址
// 设置验证码图片标签的src
generateImageCode(); // 生成图像验证码图片
$img.click(generateImageCode); // 点击图片验证码生成新的图片验证码图片
// 2、用户名验证逻辑
$username.blur(function () {
fn_check_usrname();
});
// 3、手机号验证逻辑
// 判断用户手机号是否注册
$mobile.blur(function () {
fn_check_mobile();
});
// 4、发送短信验证码逻辑
$smsCodeBtn.click(function () {
// 判断手机号是否输入
if (fn_check_mobile() !== "success") {
return
}
// 判断用户是否输入图片验证码
let text = $imgCodeText.val(); // 获取用户输入的图片验证码文本
if (!text) {
message.showError('请填写验证码!');
return
}
if (!sImageCodeId) {
message.showError('图片UUID为空');
return
}
// 正常
let SdataParams = {
"mobile": $mobile.val(), // 获取用户输入的手机号
"text": text, // 获取用户输入的图片验证码文本
"image_code_id": sImageCodeId // 获取图片UUID
};
// for test
// let SdataParams = {
// "mobile": "1806508", // 获取用户输入的手机号
// "text": "ha3d", // 获取用户输入的图片验证码文本
// "image_code_id": "680a5a66-d9e5-4c3c-b8ea" // 获取图片UUID
// };
// 向后端发送请求
$.ajax({
// 请求地址
url: "/sms_codes/",
// 请求方式
type: "POST",
// 向后端发送csrf token
// headers: {
// // 根据后端开启的CSRFProtect保护,cookie字段名固定为X-CSRFToken
// "X-CSRFToken": getCookie("csrf_token")
// },
// data: JSON.stringify(SdataParams),
data: JSON.stringify(SdataParams),
// 请求内容的数据类型(前端发给后端的格式)
contentType: "application/json; charset=utf-8",
// 响应数据的格式(后端返回给前端的格式)
dataType: "json",
async: false
})
.done(function (res) {
if (res.errno === "0") {
// 倒计时60秒,60秒后允许用户再次点击发送短信验证码的按钮
message.showSuccess('短信验证码发送成功');
let num = 60;
// 设置一个计时器
let t = setInterval(function () {
if (num === 1) {
// 如果计时器到最后, 清除计时器对象
clearInterval(t);
// 将点击获取验证码的按钮展示的文本恢复成原始文本
$smsCodeBtn.html("获取验证码");
} else {
num -= 1;
// 展示倒计时信息
$smsCodeBtn.html(num + "秒");
}
}, 1000);
} else {
message.showError(res.errmsg);
}
})
.fail(function(){
message.showError('服务器超时,请重试!');
});
});
// 5、注册逻辑
$register.submit(function (e) {
// 阻止默认提交操作
e.preventDefault();
// 获取用户输入的内容
let sUsername = $username.val(); // 获取用户输入的用户名字符串
let sPassword = $("input[name=password]").val();
let sPasswordRepeat = $("input[name=password_repeat]").val();
let sMobile = $mobile.val(); // 获取用户输入的手机号码字符串
let sSmsCode = $("input[name=sms_captcha]").val();
// 判断用户名是否已注册
if (fn_check_usrname() !== "success") {
return
}
// 判断手机号是否为空,是否已注册
if (fn_check_mobile() !== "success") {
return
}
// 判断用户输入的密码是否为空
if ((!sPassword) || (!sPasswordRepeat)) {
message.showError('密码或确认密码不能为空');
return
}
// 判断用户输入的密码和确认密码长度是否为6-20位
if ((sPassword.length < 6 || sPassword.length > 20) ||
(sPasswordRepeat.length < 6 || sPasswordRepeat.length > 20)) {
message.showError('密码和确认密码的长度需在6~20位以内');
return
}
// 判断用户输入的密码和确认密码是否一致
if (sPassword !== sPasswordRepeat) {
message.showError('密码和确认密码不一致');
return
}
// 判断用户输入的短信验证码是否为6位数字
if (!(/^\d{6}$/).test(sSmsCode)) {
message.showError('短信验证码格式不正确,必须为6位数字!');
return
}
// 发起注册请求
// 1、创建请求参数
let SdataParams = {
"username": sUsername,
"password": sPassword,
"password_repeat": sPasswordRepeat,
"mobile": sMobile,
"sms_code": sSmsCode
};
// 2、创建ajax请求
$.ajax({
// 请求地址
url: "/users/register/", // url尾部需要添加/
// 请求方式
type: "POST",
data: JSON.stringify(SdataParams),
// 请求内容的数据类型(前端发给后端的格式)
contentType: "application/json; charset=utf-8",
// 响应数据的格式(后端返回给前端的格式)
dataType: "json",
})
.done(function (res) {
if (res.errno === "0") {
// 注册成功
message.showSuccess('恭喜你,注册成功!');
setTimeout(function () {
// 注册成功之后重定向到主页
window.location.href = document.referrer;
}, 1000)
} else {
// 注册失败,打印错误信息
message.showError(res.errmsg);
}
})
.fail(function(){
message.showError('服务器超时,请重试!');
});
});
// 生成一个图片验证码的编号,并设置页面中图片验证码img标签的src属性
function generateImageCode() {
// 1、生成一个图片验证码随机编号
sImageCodeId = generateUUID();
// 2、拼接请求url /image_codes/<uuid:image_code_id>/
let imageCodeUrl = "/image_codes/" + sImageCodeId + "/";
// 3、修改验证码图片src地址
$img.attr('src', imageCodeUrl)
}
// 生成图片UUID验证码
function generateUUID() {
let d = new Date().getTime();
if (window.performance && typeof window.performance.now === "function") {
d += performance.now(); //use high-precision timer if available
}
let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
let r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
}
// 判断用户名是否已经注册
function fn_check_usrname() {
let sUsername = $username.val(); // 获取用户名字符串
let sReturnValue = "";
if (sUsername === "") {
message.showError('用户名不能为空!');
return
}
if (!(/^\w{5,20}$/).test(sUsername)) {
message.showError('请输入5-20个字符的用户名');
return
}
// 发送ajax请求,去后端查询用户名是否存在
$.ajax({
url: '/usernames/' + sUsername + '/',
type: 'GET',
dataType: 'json',
async: false
})
.done(function (res) {
if (res.data.count !== 0) {
message.showError(res.data.username + '已注册,请重新输入!')
sReturnValue = ""
} else {
message.showInfo(res.data.username + '能正常使用!')
sReturnValue = "success"
}
})
.fail(function () {
message.showError('服务器超时,请重试!');
sReturnValue = ""
});
return sReturnValue
}
// 判断手机号是否注册
function fn_check_mobile() {
let sMobile = $mobile.val(); // 获取用户输入的手机号码字符串
let sReturnValue = "";
if (sMobile === "") {
message.showError('手机号不能为空!');
return
}
if (!(/^1[345789]\d{9}$/).test(sMobile)) {
message.showError('手机号码格式不正确,请重新输入!');
return
}
$.ajax({
url: '/mobiles/' + sMobile + '/',
type: 'GET',
dataType: 'json',
async: false
})
.done(function (res) {
if (res.data.count !== 0) {
message.showError(res.data.mobile + '已注册,请重新输入!')
sReturnValue = ""
} else {
message.showSuccess(res.data.mobile + '能正常使用!');
sReturnValue = "success"
}
})
.fail(function () {
message.showError('服务器超时,请重试!');
sReturnValue = ""
});
return sReturnValue
}
// get cookie using jQuery
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
let cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
let cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
// Setting the token on the AJAX request
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
}
}
});
});
```