【Django 026】利用Pillow生成验证码图片结合session用于登录验证图文详解

光是用户名和密码的登陆验证方式还是很难阻止爬虫的步伐,毕竟cookie或session等会话机制的存在,使得爬虫在自动登陆后变得一劳永逸。因为不借助外力爬虫本身是很难去识别图片信息的,所以如果能结合图片验证码,会使得反爬变得更容易一点。这一篇文章我们就一起来学习下如何自己绘制出图片验证码,以及如何在登陆中应用它。

我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。

操作环境

先总结下我的操作环境:

  • Centos 7
  • Python 3.7
  • Pycharm 2019.3
  • Django 2.2
  • Pillow 5.3

因为Django长期支持版本2.2LTS和前一个长期支持版本1.11LTS有许多地方不一样,需要小心区分。

Pillow绘图基本操作

虽然说有很多插件能帮助我们完成验证码这个事情,但是知其然还得知其所以然,我们通过Pillow原生绘图来理清验证码绘制和验证的原理有助于我们处理更多的绘图相关的需求。

工欲善其事必先利其器,首先来看一下Pillow这个工具画图的基本操作。

准备工具

有三个对象要准备好:

  • Image对象,可以理解为一块画布
  • ImageDraw对象,可以理解为一块画笔,是绘图时的主要操作对象,需要和Image对象绑定
  • ImageFont对象,字体对象

首先从Pillow导入这三个类,并生成实例,需要注意的是因为历史遗留问题,Python3里面虽然安装的是Pillow,但是导入的时候还是从PIL进行导入

from PIL import Image, ImageDraw, ImageFont

生成画布命令如下

Image.new(mode,size,color=0)
  • mode可以是彩色图片用到的RGB模式,也可以是计算机处理常用的灰度图片的L模式。如果是RGB模式,所有的颜色都是三元素元组,如果是L模式,所有的颜色都是单值
  • size是一个二元元组,分别是pixel为单位的宽和高。需要注意计算机中的坐标都是以左上角为原点,左边为x,下面为y
  • color是画布的背景色,根据前面的mode进行设置

例如生成一块白色画布

mode = 'RGB'
bg_size = (300, 100)
bg_color = (255, 255, 255)

img = Image.new(mode, bg_size, bg_color)
img.show()

之后显示如下
1-image.png

生成画笔命令如下

ImageDraw.Draw(im, mode=None)
  • im是一个Image对象,画笔必须要和画布绑定才能操作
  • mode填一个和画布一样的模式即可

生成一个画笔,绑定上面的画布

draw = ImageDraw.Draw(img, mode)

生成字体命令如下

ImageFont.truetype(
    ['font=None', 'size=10', 'index=0', "encoding=''", 'layout_engine=None'],
)

通常指定字体和大小即可,而且实际赋值的时候不用写为list

  • font是一个文件或类文件对象,格式为TrueType Font(.ttf)。如果是centos7,字体保存在/usr/share/fonts/目录下
  • size是字体的大小

生成一个字体如下

font = ImageFont.truetype('/usr/share/fonts/google-crosextra-caladea/Caladea-Bold.ttf', 100)

工具准备妥当,下面开始正式绘制。

添加文字

添加文字命令如下,其中draw是上面的画笔对象

draw.text(xy,text,fill=None,font=None)
  • xy是一个二元元组,是文字的左上角坐标
  • text是文字内容,字符串对象
  • fill是文字颜色,根据画笔的mode设定,如果是RGB就是三元元组
  • font就是ImageFont对象

按照如下设定写一段文字

xy = (0,0)
fill = (209, 115, 76)
draw.text(xy,'xiaofu',fill,font)
img.show()

结果如下
2-text.png

添加色彩

添加色彩只能一个pixel接一个pixel的去填充,格式如下,其中draw是上面的画笔对象

draw.point(xy,fill=None)
  • xy是一个二元元组,是像素点的位置
  • fill是像素点的颜色,根据画笔的mode设定,如果是RGB就是三元元组

给背景添加一些随机颜色

for x in range(300):
    for y in range(100):
        fill = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
        draw.point((x, y), fill=fill)
img.show()

结果如下
3-color.png

学习了文字和色彩的绘制,下面就可以结合两者来绘制验证码了。

绘制验证码

在项目中创建一个目录utils专门放置一些和具体业务没啥关系的小工具,在其中新建verification_code.py用来放置生成验证码相关的代码。

完整代码如下

import random

from PIL import Image, ImageFont, ImageDraw


def get_code():
    mode = 'RGB'
    bg_width = 300
    bg_height = 100
    bg_size = (bg_width, bg_height)
    bg_color = (255, 255, 255)
    ttf_path = '/usr/share/fonts/google-crosextra-caladea/Caladea-Bold.ttf'
    img = Image.new(mode, bg_size, bg_color)
    draw = ImageDraw.Draw(img, mode)
    font = ImageFont.truetype(ttf_path, 80)

    # generate text
    letters = get_letters()
    for index, text in enumerate(letters):
        x = 75*index+10
        y = 0
        draw.text((x,y),text,get_rdmcolor(),font)

    # blur the background
    for i in range(20000):
        x = random.randint(0,bg_width)
        y = random.randint(0,bg_height)
        fill = get_rdmcolor()
        draw.point((x,y),fill)
    return img,letters


def get_letters():
    base = '1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM'
    result = []
    for i in range(4):
        result.append(random.choice(base))
    return result


def get_rdmcolor():
    return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)

其中get_letters用来生成一个包含四个字符的list,用来显示在验证码中,get_rdmcolor用来生成一个随机元素的三元元组表示RGB值,画图的主要逻辑是在get_code里面。

首先根据get_letters返回的list,每个字符间隔一段距离排放在画布上。然后在画布上的随机20000个点涂上随机的颜色。不仅要返回图像文件,还要同时返回四个字符用来做验证码的验证。

然后创建路由和view函数来获取一下这个验证码

path('verification_code/', views.verification_code, name='verification_code'),
def verification_code(request):
    img, letters = get_code()
    fp = BytesIO()
    img.save(fp, 'png')
    print(letters)
    return HttpResponse(fp.getvalue(), content_type='image/png')

没必要将验证码这种临时的文件保存在服务器的磁盘中,所以这里使用了BytesIO这个类文件对象用来保存图像,类文件对象的操作和磁盘上的文件差不多,不过是保存在内存中的,更详细的说明可以参考我的另一篇博客《python3中StringIO和BytesIO使用方法和使用场景详解》

类似于上传文件,返回给前端的时候是Bytes类型数据,所以也需要指定下MIME类型方便浏览器用对应程序来打开。

关于上传文件的操作可以参考《【Django 020】Django2.2多文件同时上传和文件MIME判定以及数据模型中FileField和ImageField的使用详解》

最后效果如下,每刷新一次出现一个新的验证码
4-code.png

利用验证码登录

先说一下验证码登录的验证逻辑。

  • 前端在显示登陆页面的时候会显示一张图片,src是前面的verification_code/
  • 同时会有个input标签将用户输入的验证码传递给后端
  • 后端在生成验证码的时候会将验证码同步保存到session中
  • 如果用户输入的和session内存储的一致,成功登录

下面把这个验证码添加到登陆用的login.html中,注意img标签中的src

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
<form action="{% url 'app:login' %}" method="post">
    {% csrf_token %}
    <label for="name">Name: </label><input type="text" id="name" name="name" placeholder="Your name"><br>
    <label for="verification_code">Verification Code:</label><input type="text" id="verification_code"
                                                                    name="verification_code" placeholder="Please type below code"><br>
    <img src="{% url 'app:verification_code' %}" alt="verification code"><br>
    <input type="submit">
</form>
</body>
</html>

修改下上面生成验证码的view函数如下,将letters元素连接成字符串存储在session中

def verification_code(request):
    img, letters = get_code()
    request.session['verification_code']=''.join(letters)
    fp = BytesIO()
    img.save(fp, 'png')
    return HttpResponse(fp.getvalue(), content_type='image/png')

创建登录的路由和view函数如下

path('login/', views.login, name='login'),
def login(request):
    if request.method == 'GET':
        name = request.session.get('username', 0)
        if name:
            response = HttpResponseRedirect(reverse('app:homepage'))
        else:
            response = render(request, 'login.html')
        return response
    elif request.method == 'POST':
        name = request.POST.get('name', 0)
        code = request.POST.get('verification_code','')
        if name and code == request.session.get('verification_code',''):
            response = HttpResponseRedirect(reverse('app:homepage'))
            request.session['username'] = name
        else:
            response = HttpResponseRedirect(reverse('app:login'))
        return response

此时如果请求http://127.0.0.1:8000/app/login/,结果如下
5-login.png
成功输入验证码之后跳转到用户homepage
6-homepage.png
如果验证码输入失败会重新跳转到登陆页面

点击更换验证码

随着验证码的马赛克技术越来越魔性,这东西看不清楚感觉都见怪不怪了,所以势必要给用户加一个点击替换的功能。

其逻辑如下,通过jQeuery给验证码图片添加一个点击事件,每次点击就刷新一下验证码图片的src属性。但是浏览器因为自带缓存功能,当资源的路径不发生改变默认是不会主动去服务器重新获取的,所以后面还要加一个不影响功能的随机数

在项目中创建静态文件夹static/js,新建文件login.js,内容如下

$(function () {
$('img').click(function () {
    $(this).attr('src', '/app/verification_code/?t=' + Math.random())
})

})

这里的?t=Math.random()就是为了增加一个随机数使得url不同,促使浏览器去掉缓存主动去服务端查询。

然后只需要去h5页面中添加jQuery和刚才的js脚本即可

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
            integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"
            crossorigin="anonymous"></script>
    <script type="application/javascript" src="{% static 'js/login.js' %}"></script>
</head>

之后每次点击验证码图片都会更换一张新的图,而且图片的url也会跟着换
在这里插入图片描述

总结

几个重要的知识点总结一下

  • Pillow这个库因为存在历史遗留问题,导入还是通过PIL
  • 将小工具统一放在utils目录下,减低代码的耦合性
  • 画图过程中的配置,例如长宽颜色等都可以单独做为变量来设定,提高代码的灵活度
  • 通过BytesIO将不必保存在磁盘中的内容放在内存中做为类文件来处理
  • Bytes传递到前端记得在HttpResponse中设定MIME类型,不然浏览器无法打开
  • 通过session记录用户请求过程中和用户绑定的中间变量,例如验证码
  • 浏览器的缓存原因,url不变的资源不会去主动请求,通过添加不影响请求的查询参数来解决
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值