用户登录、登出、注册功能的实现

## 用户注册功能
[TOC]
### 一、设计接口思路
- 分析业务逻辑,明确在这个业务中需要涉及到几个相关子业务,将每个子业务当做一个接口来设计
- 分析接口的功能任务,明确接口的访问方式与返回数据:
- 接口的请求方式,如GET 、POST 、PUT等
- 接口的URL路径定义
- 需要前端传递的数据及数据格式(如路径参数、查询字符串、请求体表单、JSON等)
- 返回给前端的数据及数据格式
### 二、功能分析
- 用户名判断是否存在
- 手机号判断是否存在
- 图片验证码
- 短信验证码
- 注册保存用户数据
图片验证码、短信验证码考虑到后续可能会在其他业务中也会用到,因此将验证码功能独立出来,**创建一个新应用verifications,在此应用中实现图片验证码、短信验证码**
### 三、图片验证码接口代码实现
#### 1.分析
**请求方法**:**GET**
**url定义**:`/image_codes//`
**请求参数**: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//
"""
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[\w-]+)/$', view=views.ImageCodeView.as_view(), name="image_code"),
# image_code_id为uuid格式
path('image_codes//', views.ImageCode.as_view(), name='image_code'),
]
```
#### 3.前端代码实现
html代码:
```jinja2
{# 继承base基类模版 #}
{% extends 'base/base.html' %}
{% block link %}
{##}

{% endblock %}
{% block title %}
注册
{% endblock %}

{% block main_start %}




请注册
立即登录 >

















用户登录、登出、注册功能的实现
文章图片





获取短信验证码







{% endblock %}

{% block hot_recommend %}
{% endblock %}
{% block 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//
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\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\w{5,20})/
"""
def get(self, request, username):
# count = 1 if User.objects.get(username=username) else 0
data = https://www.it610.com/article/{
'username': username,
'count': Users.objects.filter(username=username).count()
}
return to_json_data(data=https://www.it610.com/article/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=https://www.it610.com/article/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= "https://www.it610.com/article/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//', views.ImageCode.as_view(), name='image_code'),
re_path('usernames/(?P\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//
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/(?P1[3-9]\d{9})/`
**请求参数**:url路径参数
| 参数| 类型| 前端是否必须传 | 描述|
| ------ | ------ | -------------- | ---------------- |
| mobile | 字符串 | 是| 用户输入的手机号 |
#### 2.后端代码实现
```python
# 在verifications目录下的views.py文件中定义如下类视图:
class CheckMobileView(View):
"""
Check whether the mobile exists
GET mobiles/(?P1[3-9]\d{9})/
"""
def get(self, request, mobile):
data = https://www.it610.com/article/{
'mobile': mobile,
'count': Users.objects.filter(mobile=mobile).count()
}
return to_json_data(data=https://www.it610.com/article/data)
```
```python
# 在verifications目录下的urls.py文件中定义如下路由:
from django.urls import path, re_path
from . import views
app_name = "verifications"
urlpatterns = [
re_path('mobiles/(?P1[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//
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 SreturnValuehttps://www.it610.com/article/= "";
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 + '已注册,请重新输入!')
SreturnValuehttps://www.it610.com/article/= ""
} else {
SreturnValuehttps://www.it610.com/article/= "success"
}
})
.fail(function () {
message.showError('服务器超时,请重试!');
SreturnValuehttps://www.it610.com/article/= ""
});
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 = https://www.it610.com/article/request.body
if not json_data:
return to_json_data(errno=Code.PARAMERR, errmsg=error_map[Code.PARAMERR])
# 将json转化为dict
dict_data = https://www.it610.com/article/json.loads(json_data.decode('utf8'))
# 2、
form = forms.CheckImgCodeForm(data=https://www.it610.com/article/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 = https://www.it610.com/article/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.**本项目中使用的短信发送模块为**[云通讯]()平台:
- 用户发送短信(或语音)验证码的第三方平台
- [参考文档地址]()
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//
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 sReturnValuehttps://www.it610.com/article/= "";


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 + '已注册,请重新输入!');
sReturnValuehttps://www.it610.com/article/= ""
} else {
message.showInfo(res.data.username + '能正常使用!');
sReturnValuehttps://www.it610.com/article/= ""
}
})
.fail(function () {
message.showError('服务器超时,请重试!');
sReturnValuehttps://www.it610.com/article/= ""
});


return sReturnValue
}
function fn_check_mobile() {
let sMobile = $mobile.val(); // 获取用户输入的手机号码字符串
let sReturnValuehttps://www.it610.com/article/= "";
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 + '已注册,请重新输入!')
sReturnValuehttps://www.it610.com/article/= ""
} else {
SreturnValuehttps://www.it610.com/article/= "success"
}
})
.fail(function () {
message.showError('服务器超时,请重试!');
sReturnValuehttps://www.it610.com/article/= ""
});
return sReturnValue
}
// get cookie using jQuery
function getCookie(name) {
let cookieValue = https://www.it610.com/article/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 = https://www.it610.com/article/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 = https://www.it610.com/article/request.body
if not json_data:
return to_json_data(errno=Code.PARAMERR, errmsg=error_map[Code.PARAMERR])
# 将json转化为dict
dict_data = https://www.it610.com/article/json.loads(json_data.decode('utf8'))
form = RegisterForm(data=https://www.it610.com/article/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 = https://www.it610.com/article/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 = https://www.it610.com/article/document.referrer;
}, 1000)
} else {
// 注册失败,打印错误信息
message.showError(res.errmsg);
}
})
.fail(function(){
message.showError('服务器超时,请重试!');
});
});


// 生成一个图片验证码的编号,并设置页面中图片验证码img标签的src属性
function generateImageCode() {
// 1、生成一个图片验证码随机编号
sImageCodeId = generateUUID();
// 2、拼接请求url /image_codes//
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 sReturnValuehttps://www.it610.com/article/= "";
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 + '已注册,请重新输入!')
sReturnValuehttps://www.it610.com/article/= ""
} else {
message.showInfo(res.data.username + '能正常使用!')
sReturnValuehttps://www.it610.com/article/= "success"
}
})
.fail(function () {
message.showError('服务器超时,请重试!');
sReturnValuehttps://www.it610.com/article/= ""
});
return sReturnValue
}


// 判断手机号是否注册
function fn_check_mobile() {
let sMobile = $mobile.val(); // 获取用户输入的手机号码字符串
let sReturnValuehttps://www.it610.com/article/= "";
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 + '已注册,请重新输入!')
sReturnValuehttps://www.it610.com/article/= ""
} else {
message.showSuccess(res.data.mobile + '能正常使用!');
sReturnValuehttps://www.it610.com/article/= "success"
}
})
.fail(function () {
message.showError('服务器超时,请重试!');
sReturnValuehttps://www.it610.com/article/= ""
});
return sReturnValue
}
// get cookie using jQuery
function getCookie(name) {
let cookieValue = https://www.it610.com/article/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 = https://www.it610.com/article/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'));
}
}
});
});
【用户登录、登出、注册功能的实现】```

    推荐阅读