Django是目前比较火爆的框架,之前有在知乎刷到,很多毕业生进入大厂实习后因为不会git和Django框架3天就被踢掉了,因为他们很难把自己的工作融入到整个组的工作中。因此,我尝试自学Django并整理出如下笔记。
为了防止用户名和密码被暴力破解,很多网页都会采取图形验证码的方式缓解这个问题,我们页用Dango实现一下这个功能。内容包括:
- 使用pillow(PIL包)中的Image, ImageDraw, ImageFont, ImageFilter生成白画布、生成字母(字体、加粗)、加入干扰元素(点、线、圆)
- 在已有文件中加入验证码图片,并将用户输入的验证码与验证码结果对比校验
0. 既有工作
介绍一下已经完成的东西。首先是一个登录界面,但是不含有图形验证码
登录之后就是一个平平无奇的管理系统,这里不多介绍。
我们的工作主要是针对login登录界面的。
1. 生成图片
这里用的是pillow包。主要思路为:生成白画布 -> 生成字母(字体、加粗) -> 加入干扰元素(点、线、圆)。
# -*- coding:utf-8 -*-
import random
from PIL import Image, ImageDraw, ImageFont, ImageFilter
def get_image(width=128, height=38, char_length=4, font_file='application01/static/font/font.ttf', font_size=30):
code = []
# 创建画布
img = Image.new(mode='RGB', size=(width, height), color=(255, 255, 255))
draw = ImageDraw.Draw(img, mode='RGB')
def rndChar():
return chr(random.randint(65, 90))
def rndColor():
return (random.randint(0, 255), random.randint(10, 255), random.randint(64, 255))
font = ImageFont.truetype(font_file, font_size)
for i in range(char_length):
char = rndChar()
code.append(char)
h = random.randint(0, 4)
# 加粗显示
for j in range(4):
draw.text([i * width / char_length + j, h], char, font=font, fill=rndColor())
# 干扰点
for i in range(40):
draw.point([random.randint(0, width), random.randint(0, height)], fill=rndColor())
# 干扰圆
for i in range(40):
draw.point([random.randint(0, width), random.randint(0, height)], fill=rndColor())
x = random.randint(0, width)
y = random.randint(0, width)
draw.arc((x, y, x+4, y+4), 0, 90, fill=rndColor())
# 干扰线
for i in range(10):
x1 = random.randint(0, width)
y1 = random.randint(0, height)
x2 = random.randint(0, width)
y2 = random.randint(0, height)
draw.line((x1, y1, x2, y2), fill=rndColor())
img = img.filter(ImageFilter.EDGE_ENHANCE_MORE)
# print(''.join(code))
# img.show()
return img, ''.join(code)
if __name__ == '__main__':
get_image()
将整个代码放入项目。
2. 校验
考虑到多用户访问,我们采取内存读取,并给每个校验码设置超时时长。
from io import BytesIO
from application01.utils.create_image import get_image
def image_code(request):
img, code = get_image()
# 考虑到多用户访问
request.session['image_code'] = code
# 设置60秒后超时
request.session.set_expiry(60)
# 直接存内存里
stream = BytesIO()
img.save(stream, 'png')
return HttpResponse(stream.getvalue())
将这段代码加入至我们平时放用户认证的文件account.py
中:
在html前端界面中加入验证码图片和校验,重点关注注释为验证码部分
的代码:
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="http://at.alicdn.com/t/font_1786038_m62pqneyrzf.css">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
html {
height: 100%;
}
body {
height: 100%;
font-family: JetBrains Mono Medium;
display: flex;
align-items: center;
justify-content: center;
/* background-color: #0e92b3; */
background: url({% static 'img/Olsen02.jpg' %}) no-repeat;
background-size: 100%;
}
.form-wrapper {
width: 300px;
background-color: rgba(41, 45, 62, .8);
color: #fff;
border-radius: 2px;
padding: 50px;
}
.form-wrapper .header {
text-align: center;
font-size: 35px;
text-transform: uppercase;
line-height: 100px;
}
.form-wrapper .input-wrapper input {
background-color: rgb(41, 45, 62);
border: 0;
width: 100%;
text-align: center;
font-size: 15px;
color: #fff;
outline: none;
}
.form-wrapper .input-wrapper input::placeholder {
text-transform: uppercase;
}
.form-wrapper .input-wrapper .border-wrapper {
background-image: linear-gradient(to right, #e8198b, #0eb4dd);
width: 100%;
height: 50px;
margin-bottom: 20px;
border-radius: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.form-wrapper .input-wrapper .border-wrapper .border-item {
height: calc(100% - 4px);
width: calc(100% - 4px);
border-radius: 30px;
}
.form-wrapper .action {
display: flex;
justify-content: center;
}
.form-wrapper .action .btn {
width: 60%;
text-transform: uppercase;
border: 2px solid #0e92b3;
text-align: center;
line-height: 50px;
border-radius: 30px;
cursor: pointer;
}
.form-wrapper .action .btn:hover {
background-image: linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%);
}
.form-wrapper .icon-wrapper {
text-align: center;
width: 60%;
margin: 0 auto;
margin-top: 20px;
border-top: 1px dashed rgb(146, 146, 146);
padding: 20px;
}
.form-wrapper .icon-wrapper i {
font-size: 20px;
color: rgb(187, 187, 187);
cursor: pointer;
border: 1px solid #fff;
padding: 5px;
border-radius: 20px;
}
.form-wrapper .icon-wrapper i:hover {
background-color: #0e92b3;
}
.block {
background-color: transparent;
color: #fff;
font-family: JetBrains Mono Medium;
display: block;
font-size: 16px;
margin-top:15px;
}
</style>
</head>
<body>
<div class="form-wrapper">
<div class="header">
login
</div>
<form method="post">
{% csrf_token %}
<div class="input-wrapper">
<div class="border-wrapper">
<!-- <input type="text" name="username" placeholder="username" class="border-item" autocomplete="off">-->
{{ form.username }}
</div>
<div style="text-align: center; width: calc(100% - 4px);">{{ form.username.errors.0 }}</div>
<div class="border-wrapper">
<!-- <input type="password" name="password" placeholder="password" class="border-item" autocomplete="off">-->
{{ form.password }}
</div>
<div style="text-align: center; width: calc(100% - 4px);">{{ form.password.errors.0 }}</div>
</div>
<!--验证码部分-->
<div class="form-group">
<div class="row">
<div class="col-xs-7">
<div class="input-wrapper">
<div class="border-wrapper">
{{ form.code }}
<span style="color: red;"></span>
</div>
</div>
</div>
<div class="col-xs-5">
<a href="/login"><img id="image_code" src="/image/code"></a>
<span style="color: #898989;"> 点击图片换一张</span>
<div style="text-align: center; width: 50%; color: red">{{ form.code.errors.0 }}</div>
</div>
</div>
</div>
<div class="action">
<!-- <div class="btn" >login</div>-->
<input type="submit" class="btn block" value="LOGIN"></input>
</div>
</form>
<div class="icon-wrapper">
<i class="iconfont icon-weixin"></i>
<i class="iconfont icon-qq"></i>
<i class="iconfont icon-git"></i>
</div>
</div>
</body>
</html>
最后,在已有的登录界面中加入这段验证码图片生成代码,并在用户输入后校验输入的结果与验证码本身。这段代码大部分属于已有工作,详见07 用户管理。
# 登录界面
def login(request):
if request.method == 'GET':
form = LoginForm()
return render(request, 'login.html', {"form": form})
form = LoginForm(data=request.POST)
if form.is_valid():
# 校验验证码
input_code = form.cleaned_data.pop('code')
true_code = request.session.get('image_code', '')
if input_code.upper() != true_code.upper():
form.add_error('code', '验证码错误')
return render(request, 'login.html', {'form': form})
# 这里是没有models.save的,但是form.cleaned_data
# 直接用字典形式,不用一个一个写
admin_exist = models.Admin.objects.filter(**form.cleaned_data).first()
# 密码错误
if not admin_exist:
form.add_error('password', '用户名或密码错误')
return render(request, 'login.html', {'form': form})
# 生成随机cookie原来还是比较麻烦的事情,但是Django帮我们简化了
# 这一行代码直接完成了cookie的随机字符串的生成,将随机字符串保存到session中并附上响应的值
request.session['info'] = {'id': admin_exist.id, 'username': admin_exist.username}
# 7天免登录
request.session.set_expiry(60 * 60 * 24 * 7)
return redirect("/admin/list")
return render(request, 'login.html', {"form": form})
3. 结果展示
因为文件大小限制,图片画质有点离谱。