文章目录
用户模型设计
- 当前用户系统必须要存入的字段:用户名,密码,手机号
- 添加用户手机号字段,添加邮箱状态字段
- auth_user中有create_superuser方法,把这个方法按自己意愿改
- create_superuser方法把email字段去掉,添加mobile字段 - 在settings.py文件中添加如下配置:
#自定义用户模型
AUTH_USER_MODEL = 'users.Users #app名.模型类名
users/views.py
from django.shortcuts import render
from django.views import View #用模型类
#用到渲染模板和提交数据用类视图时比较多
class RegisterView(View):
"""
#一般会注释url
/register/
"""
def get(self,request):
return render(request,'users/register.html')
users/urls.py
from django.urls import path, re_path
from users import views
app_name = 'users'
urlpatterns = [
path('login/',views.login,name='login'),
#模型类的路径用views.类名.as_view()
path('register/',views.RegisterView.as_view(),name='register'),
]
users/models.py
from django.db import models
#把UserManager类重命名_UserManager,用于重写
from django.contrib.auth.models import AbstractUser, UserManager as _UserManager
class UserManager(_UserManager): #继承框架的UserManager
"""
define user manager for modifing 'no need email'
定义用于修改“无需电子邮件”的用户管理器当运行
when 'python manager.py createsuperuser'python manager.py createsuperuser
"""
#设置email的默认值为None
def create_superuser(self,username,password,email=None,**extra_fields):
"""
只是把email字段默认为空,方法里其他不变
:param username:
:param password:
:param email:
:param extra_fields:
:return:
"""
# super(UserManager,self).create_superuser(username=username,
#沿用父类的方法
super().create_superuser(username=username, #这行代码是python3中的写法,上行是python2中的写法
password=password,
email=email,
**extra_fields)
class Users(AbstractUser): #AbstractUser是django的基类
"""
add mobile,email_active fields to django users models
"""
objects = UserManager()
# A list of the field names that will be prompted for
#通过createSuperUser管理命令创建用户时将提示输入的字段名称列表
# when creating a user via the createsuperuser management command
# REQUIRED_FIELS是AbstractUser提供的参数,
# 除了用户名和密码是必须的,其他的想要添加的字段往参数里添加
REQUIRED_FIELDS = ['mobile']
# help_test在api接口文档中会用到
# verbose_name在admin站点中会用到
#增加手机号验证字段
mobile = models.CharField(max_length=11,unique=True,verbose_name='手机号',
#Django 模型中的verbose_name我们常常可能需要使用。
#比如将数据库里面的数据导出成csv文件,那么csv文件的表头的名字可以通过取每个字段的verbose_name来获取,数据可以通过queryset语句来获取。
# 这样制作出来的csv表就能想数据库一样,字段名和字段值一一对应了
help_text='手机号',
error_messages={'unique':'此手机号已被注册'}
) #指定报错的中文信息
# 邮箱状态默认为关闭,本项目不用邮箱验证,用手机号验证
email_active = models.BooleanField(default=False,verbose_name='邮箱验证状态')
#元类
class Meta:
db_table = 'tb_users' #指明数据库表名
verbose_name = '用户' #在admin站点中显示的名称
verbose_name_plural = verbose_name #显示的复数名称
def __str__(self): #打印对象时调用
return self.username
用户注册功能
设计接口思路
- 分析业务逻辑,明确在这个业务中需要涉及到几个相关子业务,将每个子业务当作一个接口来设计
- 分析接口的功能任务,明确接口的访问方式与返回数据
- 接口的请求方式,如GET,POST,PUT等
- 接口的URL路径定义
- 需要前端传递的数据及数据格式(如路径参数,查询字符串,请求体表单,JSON等)
- 返回给前端的数据及数据格式
功能分析
- 用户名判断是否存在
- 手机号判断是否存在
- 图片验证码
- 短信验证码
- 注册保存用户数据
图片验证码,短信验证码考虑到后续可能会在其他业务中也用到,因此将验证码功能独立出来,创建一个新应用verifications,在此应用中实现图片验证码,短信验证码
图片验证码接口代码实现
图片验证码认证
- 请求方法:GET
- url定义:/image_codes/<uuid:image_code_id>/
- 请求参数:url路径参数
参数 | 类型 | 前端是否必须传 | 描述 |
---|---|---|---|
image_codes | uuid字符串 | 是 | 图片验证码编号 |
后端视图实现
- 直接借鉴大牛的代码,将生成图片验证码的模块文件夹(captcha)复制粘贴到项目根目录utils文件夹下
- captcha下需要安装pillow,
pip install pillow
- 由于验证(图片验证,短信验证)功能,以后有可能在其他应用或项目中重用,所以单独创建一个应用来实现所有验证相关的业务逻辑接口,在apps目录中创建一个verifications应用,并在settings.py文件中的INSTALLED_APPS列表中注册
captcha.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import random
import string
import os.path
from io import BytesIO
from PIL import Image
from PIL import ImageFilter
from PIL.ImageDraw import Draw
from PIL.ImageFont import truetype
class Bezier:
def __init__(self):
self.tsequence = tuple([t / 20.0 for t in range(21)])
self.beziers = {}
def pascal_row(self, n):
""" Returns n-th row of Pascal's triangle
"""
result = [1]
x, numerator = 1, n
for denominator in range(1, n // 2 + 1):
x *= numerator
x /= denominator
result.append(x)
numerator -= 1
if n & 1 == 0:
result.extend(reversed(result[:-1]))
else:
result.extend(reversed(result))
return result
def make_bezier(self, n):
""" Bezier curves:
http://en.wikipedia.org/wiki/B%C3%A9zier_curve#Generalization
"""
try:
return self.beziers[n]
except KeyError:
combinations = self.pascal_row(n - 1)
result = []
for t in self.tsequence:
tpowers = (t ** i for i in range(n))
upowers = ((1 - t) ** i for i in range(n - 1, -1, -1))
coefs = [c * a * b for c, a, b in zip(combinations,
tpowers, upowers)]
result.append(coefs)
self.beziers[n] = result
return result
class Captcha(object):
def __init__(self):
self._bezier = Bezier()
self._dir = os.path.dirname(__file__)
# self._captcha_path = os.path.join(self._dir, '..', 'static', 'captcha')
@staticmethod
def instance():
if not hasattr(Captcha, "_instance"):
Captcha._instance = Captcha()
return Captcha._instance
def initialize(self, width=200, height=75, color=None, text=None, fonts=None):
# self.image = Image.new('RGB', (width, height), (255, 255, 255))
self._text = text if text else random.sample(string.ascii_uppercase + string.ascii_uppercase + '3456789', 4)
self.fonts = fonts if fonts else \
[os.path.join(self._dir, 'fonts', font) for font in ['Arial.ttf', 'Georgia.ttf', 'actionj.ttf']]
self.width = width
self.height = height
self._color = color if color else self.random_color(0, 200, random.randint(220, 255))
@staticmethod
def random_color(start, end, opacity=None):
red = random.randint(start, end)
green = random.randint(start, end)
blue = random.randint(start, end)
if opacity is None:
return red, green, blue
return red, green, blue, opacity
# draw image
def background(self, image):
Draw(image).rectangle([(0, 0), image.size], fill=self.random_color(238, 255))
return image
@staticmethod
def smooth(image):
return image.filter(ImageFilter.SMOOTH)
def curve(self, image, width=4, number=6, color=None):
dx, height = image.size
dx /= number
path = [(dx * i, random.randint(0, height))
for i in range(1, number)]
bcoefs = self._bezier.make_bezier(number - 1)
points = []
for coefs in bcoefs:
points.append(tuple(sum([coef * p for coef, p in zip(coefs, ps)])
for ps in zip(*path)))
Draw(image).line(points, fill=color if color else self._color, width=width)
return image
def noise(self, image, number=50, level=2, color=None):
width, height = image.size
dx = width / 10
width -= dx
dy = height / 10
height -= dy
draw = Draw(image)
for i in range(number):
x = int(random.uniform(dx, width))
y = int(random.uniform(dy, height))
draw.line(((x, y), (x + level, y)), fill=color if color else self._color, width=level)
return image
def text(self, image, fonts, font_sizes=None, drawings=None, squeeze_factor=0.75, color=None):
color = color if color else self._color
fonts = tuple([truetype(name, size)
for name in fonts
for size in font_sizes or (65, 70, 75)])
draw = Draw(image)
char_images = []
for c in self._text:
font = random.choice(fonts)
c_width, c_height = draw.textsize(c, font=font)
char_image = Image.new('RGB', (c_width, c_height), (0, 0, 0))
char_draw = Draw(char_image)
char_draw.text((0, 0), c, font=font, fill=color)
char_image = char_image.crop(char_image.getbbox())
for drawing in drawings:
d = getattr(self, drawing)
char_image = d(char_image)
char_images.append(char_image)
width, height = image.size
offset = int((width - sum(int(i.size[0] * squeeze_factor)
for i in char_images[:-1]) -
char_images[-1].size[0]) / 2)
for char_image in char_images:
c_width, c_height = char_image.size
mask = char_image.convert('L').point(lambda i: i * 1.97)
image.paste(char_image,
(offset, int((height - c_height) / 2)),
mask)
offset += int(c_width * squeeze_factor)
return image
# draw text
@staticmethod
def warp(image, dx_factor=0.27, dy_factor=0.21):
width, height = image.size
dx = width * dx_factor
dy = height * dy_factor
x1 = int(random.uniform(-dx, dx))
y1 = int(random.uniform(-dy, dy))
x2 = int(random.uniform(-dx, dx))
y2 = int(random.uniform(-dy, dy))
image2 = Image.new('RGB',
(width + abs(x1) + abs(x2),
height + abs(y1) + abs(y2)))
image2.paste(image, (abs(x1), abs(y1)))
width2, height2 = image2.size
return image2.transform(
(width, height), Image.QUAD,
(x1, y1,
-x1, height2 - y2,
width2 + x2, height2 + y2,
width2 - x2, -y1))
@staticmethod
def offset(image, dx_factor=0.1, dy_factor=0.2):
width, height = image.size
dx = int(random.random() * width * dx_factor)
dy = int(random.random() * height * dy_factor)
image2 = Image.new('RGB', (width + dx, height + dy))
image2.paste(image, (dx, dy))
return image2
@staticmethod
def rotate(image, angle=25):
return image.rotate(
random.uniform(-angle, angle), Image.BILINEAR, expand=1)
def captcha(self, path=None, fmt='JPEG'):
"""Create a captcha.
Args:
path: save path, default None.
fmt: image format, PNG / JPEG.
Returns:
A tuple, (text, StringIO.value).
For example:
('JGW9', '\x89PNG\r\n\x1a\n\x00\x00\x00\r...')
"""
image = Image.new('RGB', (self.width, self.height), (255, 255, 255))
image = self.background(image)
image = self.text(image, self.fonts, drawings=['warp', 'rotate', 'offset'])
image = self.curve(image)
image = self.noise(image)
image = self.smooth(image)
text = "".join(self._text)
out = BytesIO()
image.save(out, format=fmt)
return text, out.getvalue()
def generate_captcha(self):
self.initialize()
return self.captcha("")
captcha = Captcha.instance() #verifications的views.py文件导入此方法
if __name__ == '__main__':
print(captcha.generate_captcha())
- 运行captcha.py,得到下图所示,是一个元组,形如:(‘文本’,‘图片’)
verifications/views.py
from django.shortcuts import render
from django.http import HttpResponse
from django_redis import get_redis_connection #连接redis数据库
from utils.captcha.captcha import captcha #utils/captcha/captcha.py captcha实例
from django.views import View
from apps.verifications import constants
import logging
#导入settings中的日志器
logger = logging.getLogger('django')
class ImageCode(View):
"""
/image_codes/<uuid:image_code_id>/
图形验证码需要标号后存入redis,用于匹配
前端请求方法:get
"""
def get(self,request,image_code_id):
# 见captcha.py的最后一行,即输出的元组
text,image = captcha.generate_captcha() #图片展示给用户看,text要临时存入数据库
con_redis = get_redis_connection(alias='verity_codes') #连接指定的redis库
# img_key = 'img_{}'.format(image_code_id) #把uuid拼接成img_xxx形式
img_key = f'img_{image_code_id}' #等同于上行代码
# con_redis.setex(img_key,300,text) #暂存300s
# 把带uuid的拼接形式,暂存时间,验证码的text文本形式保存进redis
con_redis.setex(img_key,constants.IMAGE_CODE_REDIS_EXPIRES,text) #常量一般单独放置,尽量不改源代码,方便以后修改维护
logger.info('Image_code: {}'.format(text)) #后台日志直接打印,方便调试
return HttpResponse(content=image,content_type='image/jpg')
- 在settings.py文件中加入如下代码:
# 将用户的session保存到redis中
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
# 指定缓存redis的别名
SESSION_CACHE_ALIAS = "session"
verifications/urls.py
from django.urls import path, re_path
from verifications import views
app_name = 'verifications'
urlpatterns = [
path('image_codes/<uuid:image_code_id>/',views.ImageCode.as_view(),name='verifications'),
]
- 在web_prv/urls.py中添加verifications.urls
users/register
- 两处改动
- 验证码的img标签的src改为空
- 在结尾加上auth.js文件
{% extends 'base/base.html' %}
{% block title %}RegisterPage{% endblock %}
{% block link %}<link rel="stylesheet" href="{% static 'css/authPro/auth.css' %}">{% endblock %}
<!-- container 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="请输入用户名" id="user_name" name="username" class="form-control" autocomplete="off">
</div>
<div class="form-item">
<input type="password" placeholder="请输入密码" id="pwd" 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="请输入手机号" id="mobile" name="telephone" class="form-control" autocomplete="off" autofocus>
</div>
<div class="form-item">
<input type="text" placeholder="请输入图形验证码" id="input_captcha" 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" id="input_smscode" 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 %}
<!-- container end -->
{% block domready %}
<script src="{% static 'js/users/auth.js' %}"></script>
{% endblock %}
- 每生成一张图形验证码就在数据库存上对应的text,对每一张图形验证码进行编号,使其与对应的text为一组,以便刷新多个验证码时可以对应上
- 图形验证码是给客户看的,对应的text是和客户输入的验证码进行对比的
- 在static/js/新建文件夹users,在users中新建auth.js
- auth.js
$(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;
}
});