实现“用户登录”的功能,并限制只有登录的用户才能投票
创建用户模型。之前我们讲解过如果通过Django的ORM实现从二维表到模型的转换(反向工
程),这次我们尝试把模型变成二维表(正向工程)。
class User(models.Model):
"""用户"""
no = models.AutoField(primary_key=True,verbose_name='编号')
username = models.CharField(max_length=20,unique=True,verbose_name='用户名')
password = models.CharField(max_length=32,verbose_name='密码')
tel = models.CharField(max_length=20,verbose_name='手机号').
reg_date = models.DateTimeField(auto_now_add=True,verbose_name='注册时间')
last_visit = models.DateTimeField(null=True,verbose_name='最后登陆时间')
#用户密码要在数据库中需要保存为哈希摘要(签名,指纹)
#Django框架自带哈希256函数 make_password() check_password()
class Meta:
managed = True
db_table = 'tb_user'
verbose_name = '用户'
verbose_name_plural = '用户'
使用下面的命令生成迁移文件并执行迁移,将 User 模型直接变成关系型数据库中的二维
表 tb_user 。
python manage.py makemigrations polls #生成迁移(执行完后,polls下会多出migrations文件夹)
python manage.py migrate polls #执行迁移迁移managed = True的模型
#完成后在数据库查看下表(只可以迁移一次)
#如果迁移错误的话,在数据库 django_migrations表中找到刚才迁移的记录,删除即可再次迁移
#使用sql语句在数据库中添加两条用户数据测试
insert into `tb_user`
(`username`, `password`, `tel`, `reg_date`)
values
('wangdachui', '1c63129ae9db9c60c3e8aa94d3e00495', '13122334455', now()),
('hellokitty', 'c6f8cf68e5f68b0aa4680e089ee4742c', '13890006789', now());
说明:上面创建的两个用户 wangdachui 和 hellokitty 密码分别是 1qaz2wsx 和 Abc123!! 。
在应用下增加一个名为 utils.py 的模块用来保存需要使用的工具函数。Python标准库中的
hashlib 模块封装了常用的哈希算法,包括:MD5、SHA1、SHA256等。下面是使用hashlib 中 的 md5 类将字符串处理成MD5摘要的函数如下所示。
import hashlib
def gen_md5_digest(content):
return hashlib.md5(content.encode()).hexdigest()
进入登陆页面和注册页面
修改view.py可以可以在首页进入登录或者注册页面
def login(request: HttpResponse) -> HttpResponse:
return render(request,'login.html')
def register(request: HttpResponse) -> HttpResponse:
return render(request,'register.html')
修改urls.py中数据,添加两个链接
path('login/',login),
path('register/',register)
修改subject.html(跳转注册和登陆的超链接)
<div class="user">
<a href="/login/">用户登录</a> //改为url,点击后访问login视图函数
<a href="/register/">快速注册</a> //同上,可以进入注册页面
</div>
#同上方法修改login.html中最后返回首页和注册新用户的超链接
<div>
<a href="/">返回首页</a>
<a href="/register/">注册新用户</a>
</div>
#同上方法修改register.html中返回首页和返回登陆页的超链接
<div>
<a href="/">返回首页</a>
<a href="/login/">返回登录</a>
</div>
#昨晚此步骤整个网站流程可以跑通
生成验证码的代码
在应用(polls)下创建fonts文件夹用于存放字体,在应用(polls)下创建captcha.py用于生成验证码
准备三个字体文件ttf格式(我准备了三个Action.ttf 、Arial.ttf 、 Gerorgia.ttf
"""
图片验证码
"""
import os
import random
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 make_bezier(self, n):
"""绘制贝塞尔曲线"""
try:
return self.beziers[n]
except KeyError:
combinations = 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:
"""验证码"""
def __init__(self, width, height, fonts=None, color=None):
self._image = None
self._fonts = fonts if fonts else \
[os.path.join(os.path.dirname(__file__), 'fonts', font)
for font in ['Arial.ttf', 'Georgia.ttf', 'Action.ttf']]
self._color = color if color else random_color(0, 200, random.randint(220, 255))
self._width, self._height = width, height
@classmethod
def instance(cls, width=200, height=75):
prop_name = f'_instance_{width}_{height}'
if not hasattr(cls, prop_name):
setattr(cls, prop_name, cls(width, height))
return getattr(cls, prop_name)
def _background(self):
"""绘制背景"""
Draw(self._image).rectangle([(0, 0), self._image.size],
fill=random_color(230, 255))
def _smooth(self):
"""平滑图像"""
return self._image.filter(ImageFilter.SMOOTH)
def _curve(self, width=4, number=6, color=None):
"""绘制曲线"""
dx, height = self._image.size
dx /= number
path = [(dx * i, random.randint(0, height))
for i in range(1, number)]
bcoefs = 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(self._image).line(points, fill=color if color else self._color, width=width)
def _noise(self, number=50, level=2, color=None):
"""绘制扰码"""
width, height = self._image.size
dx, dy = width / 10, height / 10
width, height = width - dx, height - dy
draw = Draw(self._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)
def _text(self, captcha_text, 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(self._image)
char_images = []
for c in captcha_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 = self._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)
self._image.paste(char_image,
(offset, int((height - c_height) / 2)),
mask)
offset += int(c_width * squeeze_factor)
@staticmethod
def _warp(image, dx_factor=0.3, dy_factor=0.3):
"""图像扭曲"""
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))
warp_image = Image.new(
'RGB',
(width + abs(x1) + abs(x2), height + abs(y1) + abs(y2)))
warp_image.paste(image, (abs(x1), abs(y1)))
width2, height2 = warp_image.size
return warp_image.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)
offset_image = Image.new('RGB', (width + dx, height + dy))
offset_image.paste(image, (dx, dy))
return offset_image
@staticmethod
def _rotate(image, angle=25):
"""图像旋转"""
return image.rotate(random.uniform(-angle, angle),
Image.BILINEAR, expand=1)
def generate(self, captcha_text='', fmt='PNG'):
"""生成验证码(文字和图片)
:param captcha_text: 验证码文字
:param fmt: 生成的验证码图片格式
:return: 验证码图片的二进制数据
"""
self._image = Image.new('RGB', (self._width, self._height), (255, 255, 255))
self._background()
self._text(captcha_text, self._fonts,
drawings=['_warp', '_rotate', '_offset'])
self._curve()
self._noise()
self._smooth()
image_bytes = BytesIO()
self._image.save(image_bytes, format=fmt)
return image_bytes.getvalue()
def pascal_row(n=0):
"""生成毕达哥拉斯三角形(杨辉三角)"""
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 random_color(start=0, end=255, opacity=255):
"""获得随机颜色"""
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
目前项目的文件结构
在登陆页面和注册页面实现动态验证码
在views.py中添加验证码的视图函数
#生成验证码,需要导入这个类
from polls.captcha import Captcha
def get_captcha(request: HttpResponse) ->HttpResponse:
image_data = Captcha.instance().generate('1Qaz') #1Qaz为给出指定字符
return HttpResponse(image_data,content_type='image/png')
在urls.py中添加请求验证码的url(注意需要导入这个视图函数
path('captcha/',get_captcha)
#完成此步可以访问 http://127.0.0.1:8001/captcha/ 可以看到验证码
修改login.html中验证码
<div class="input">
<label>验证码:</label>
<input type="text" name="captcha">
<img id = "code" src="/captcha/" alt="" width="150" height="40"> //会请求captcha/这个视图函数
</div>
#添加了id方便js代码拿到标签 ,修改了src
在utils.py中添加随机验证码生成的代码
ALL_CHARS = '0123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM'
def gen_code(length=4):
return ''.join(random.choices(ALL_CHARS,k=length))
修改views.py中的验证码视图函数
def get_captcha(request: HttpResponse) ->HttpResponse:
code = gen_code() #默认返回四个随机字符
image_data = Captcha.instance().generate(code)
return HttpResponse(image_data,content_type='image/png')
增加点击验证码可以更换验证码的功能
#在login.html最后的body标签前增加js代码
</div>
<script>
let codeImage = document.querySelector('#code')
codeImage.addEventListener('click',(evt) => {
evt.target.src = '/captcha/?' + Math.random()
})
</script>
</body>
#使用jQuery来写js也可以
<!--
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
$('#code').on('click',(evt) >= {
$(evt.target).attr('src','/captcha/?' + Math.random())
</script>
-->
实现用户登陆
修改login.html中表单
<p class="hint">{{ hint }}</p> //方便输入账号密码错误时的提醒
<form action="/login/" method="post"> //enctype="multipart/form-data"
{% csrf_token %} //隐藏域,注册页面也需要加上
#给错误提醒的p标签加上css样式表,在style最后加上
.hint{
color: red;
font-size: 12px;
}
#表单用post请求/login/函数,如果表单中有文件需要加上注释的那段
注意,在上面的表单中,我们使用了模板指令 {% csrf_token %} 为表单添加一个隐藏域(可
以在浏览器中显示网页源代码就可以看到这个指令生成的 type 属性为 hidden 的 input 标签),它
的作用是在表单中生成一个随机令牌(token)来防范跨站请求伪造(简称为CSRF),这也是
Django在提交表单时的硬性要求。如果我们的表单中没有这样的令牌,那么提交表单时,Django
框架会产生一个响应状态码为 403 的响应(禁止访问),除非我们设置了免除CSRF令牌。
修改views.py中login.py函数
def login(request: HttpResponse) -> HttpResponse:
hint = ''
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
if username and password:
password = gen_md5_digest(password) #需要导入之前写的md5加密模块
user = User.objects.filter(username=username,password=password).first()
if user:
pass
else:
hint = '用户名或密码错误'
else:
hint = '请输入有效的用户名和密码'
return render(request,'login.html',{'hint':hint})
#做完此步可以测试登陆界面