公司动态

Django验证码实现:基于Pillow库的手动生成与安全集成指南

📅 2026/8/29 11:09:32
Django验证码实现:基于Pillow库的手动生成与安全集成指南
1. 项目概述与核心价值在任何一个需要用户登录或提交敏感操作的Web应用中验证码都是一个绕不开的基础安全组件。它的核心价值在于区分操作者是“真人”还是“自动化脚本”从而有效抵御暴力破解、恶意注册、刷票、垃圾评论等攻击。对于使用Django框架的开发者来说实现验证码功能是必备技能之一。虽然市面上有现成的第三方包如django-simple-captcha但很多时候我们需要的只是一个轻量、可控、不引入额外依赖的解决方案。这时直接使用Python强大的图像处理库Pillow来生成验证码就成了一个既锻炼底层能力又极具实用性的选择。这个“Django项目验证码Pillow库实现”项目就是带你从零开始手把手构建一个完全自主可控的图形验证码模块。我们将不依赖任何第三方验证码库仅使用Django自身的会话机制和Pillow的图像生成能力实现一个包含数字、字母混合并加入干扰线、干扰点等常见抗识别手段的验证码系统。整个过程你会深入理解验证码的生成逻辑、图片的绘制流程、以及如何与Django的请求-响应周期无缝集成。无论你是想为个人项目增加一道安全防线还是希望在面试中展示对Web安全基础的理解这个实战案例都将提供扎实的代码和清晰的思路。2. 技术选型与设计思路拆解2.1 为什么选择Pillow而非其他方案在Django中实现验证码通常有几条路径一是使用功能全面的第三方App二是调用付费的云API三就是像我们这样用基础库手动绘制。选择Pillow方案主要基于以下几点考量首先是可控性。第三方包如django-simple-captcha虽然开箱即用但其样式、生成逻辑、存储方式如使用数据库都是封装好的。当你有定制化需求时例如需要特定字体、生成算术验证码、或者改变图片尺寸就可能需要去阅读其源码甚至进行修改学习成本并不低。而自己用Pillow实现从画布大小、背景色、字体选择、字符扭曲到干扰元素的添加每一个像素点都由你掌控后期调整起来得心应手。其次是依赖的简洁性。Pillow是Python生态中处理图像事实上的标准库应用极其广泛。你的项目很可能已经因为其他功能如用户头像处理而安装了它。此时再引入一个专门的验证码库就增加了不必要的依赖。我们的方案只依赖Django必须和Pillow很可能已安装让项目保持清爽。最后是学习价值。通过这个实现过程你将不仅仅学会“如何使用一个验证码库”而是深入理解“验证码是如何被创造出来的”。你会接触到图像处理的基本概念、随机数的应用、会话Session机制如何用于状态保持这对你理解Web安全的其他方面也大有裨益。2.2 整体架构设计我们的验证码系统将遵循经典的“生成-存储-验证”三步流程并与Django的MVCMTV模式紧密结合生成端 (View Pillow)创建一个专门的Django视图例如/captcha/。当该视图被请求时它会在内存中随机生成一个字符串验证码文本然后使用Pillow库在内存中绘制一张包含该文本的图片。同时必须将这个生成的文本临时存储起来供后续校验。图片以HTTP响应的形式通常是image/png直接发送给前端。存储端 (Session)存储生成的验证码文本是关键且敏感的一环。绝对不能将其直接嵌入到返回给前端的图片URL或HTML中。我们选择使用Django的Session机制。Session在服务器端存储数据只为每个用户会话提供一个唯一的ID通过Cookie传递。我们将验证码文本以键值对如‘captcha_code’: ‘A3b9’的形式存入当前请求的Session中。这样验证码文本始终安全地留在服务器端。验证端 (View Logic)在需要验证码校验的视图逻辑中例如登录或注册视图当用户提交表单后我们从request.POST中获取用户输入的验证码同时从request.session中取出之前存储的正确验证码。将两者进行比对通常不区分大小写。无论验证成功与否一个良好的实践是立即清除Session中的验证码防止被重复使用一次性验证码。前端集成 (HTML Template)在前端我们只需要一个img标签其src属性指向我们的验证码生成视图的URL。通常还会在旁边放置一个“看不清换一张”的链接或按钮通过JavaScript重新请求该图片URL来实现刷新。这个设计清晰地将职责分离生成视图只负责画图业务视图负责校验Session负责安全地传递状态前端只负责展示和交互。3. 核心细节解析与实操要点3.1 Pillow绘图核心对象与流程Pillow绘图可以类比为在一张白纸画布上用笔绘图对象写字画画。主要涉及以下几个核心对象Image代表整个图像对象。我们通常从创建一张新的空白Image开始。ImageDraw绘图工具。我们需要通过ImageDraw.Draw()方法传入一个Image对象来创建一个绘图对象之后所有的绘制操作如文本、线条、点都通过这个对象进行。ImageFont字体对象。用于加载字体文件.ttf并指定文字的大小。系统字体和自定义字体均可。一个基本的绘图流程代码如下from PIL import Image, ImageDraw, ImageFont # 1. 创建画布 image Image.new(RGB, (width, height), background_color) # 2. 创建绘图工具 draw ImageDraw.Draw(image) # 3. 加载字体如果使用自定义字体 font ImageFont.truetype(path/to/font.ttf, size) # 4. 绘制文本 draw.text((x, y), text, fontfont, filltext_color) # 5. 保存或输出图像 image.save(output.png) # 或在Web中将图像存入内存字节流 from io import BytesIO buffer BytesIO() image.save(buffer, PNG) image_data buffer.getvalue()3.2 验证码安全性与用户体验的平衡验证码的核心矛盾在于既要让机器难以识别又要让人眼容易辨认。我们在设计时需要在这两者间找到平衡点。提升安全性抗机器识别的常见手段字符集选择使用数字0-9和大写字母A-Z去除容易混淆的I, O, Z等的组合增加暴力枚举的难度。避免使用小写字母l和数字1这类易混淆字符。随机扭曲与旋转对每个字符进行微小的随机旋转例如-30度到30度、随机上下偏移可以极大地增加OCR光学字符识别工具的分割和识别难度。添加干扰元素干扰线在图片上随机画几条颜色较浅、粗细不一的曲线或折线。干扰点在图片背景上随机绘制大量颜色、大小不一的像素点噪点。背景干扰使用渐变色背景或在背景上绘制低对比度的随机图形。字符粘连让字符之间的间距非常小甚至轻微重叠干扰OCR的分割算法。保障用户体验让人眼易读的要点足够的对比度文字颜色和背景色必须有明显的区分度。通常用深色字配浅色背景或反之。清晰的字体优先选择笔画清晰、无衬线的字体如Arial, Verdana的TTF文件。避免使用过于花哨或笔画细的字体。合适的长度4-6个字符是常见的长度太短不安全太长惹人烦。提供刷新机制必须提供便捷的“换一张”功能对于看不清的验证码用户应有低成本的重试机会。注意切忌为了安全而过度扭曲导致正常用户也无法识别这会直接导致用户流失。我们的实现会采用适度的扭曲和干扰。3.3 Django Session机制的正确使用Session是我们存储验证码文本的“保险箱”。理解其工作方式至关重要。原理当用户第一次访问网站时Django会创建一个唯一的Session ID并通过Set-Cookie头将其放入用户的浏览器Cookie中通常名为sessionid。同时在服务器端可以是数据库、缓存或文件Django会创建一个与该ID关联的数据存储区。后续该用户的所有请求都会携带这个CookieDjango据此找到对应的服务器端Session数据。在视图中存取在Django视图中request.session是一个类似字典的对象。存储request.session[‘captcha_code’] generated_code读取stored_code request.session.get(‘captcha_code’, ‘’删除del request.session[‘captcha_code’]或request.session[‘captcha_code’] ‘’安全性验证码文本存储在服务器端相对安全。但需注意Session劫持的风险确保Django的SECRET_KEY保密并考虑使用HTTPS。时效性Django Session有默认的过期时间如两周。但对于验证码我们需要的生命周期短得多通常是一次表单提交。因此验证后立即删除是关键步骤防止重复使用或会话固定攻击。4. 实操过程与核心环节实现4.1 环境准备与项目结构假设你已经有一个正在开发的Django项目。如果没有可以使用django-admin startproject myproject创建一个。首先确保Pillow库已安装。如果未安装使用pip安装pip install Pillow为了模块清晰我们可以在项目的主应用或者专门创建一个工具应用utils下建立一个captcha.py模块来处理验证码的生成逻辑。这样生成视图和校验视图都可以导入这个模块。一个建议的项目结构如下myproject/ ├── manage.py ├── myproject/ │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── yourapp/ # 你的主要应用比如 ‘accounts‘ ├── __init__.py ├── views.py ├── urls.py ├── utils/ │ └── captcha.py # 验证码生成核心模块 └── ...4.2 验证码生成模块captcha.py深度实现这个模块是核心我们将创建一个Captcha类来封装所有功能。# yourapp/utils/captcha.py import random import string from io import BytesIO from PIL import Image, ImageDraw, ImageFont, ImageFilter import os from django.conf import settings class Captcha: def __init__(self, width160, height60, code_length4): 初始化验证码参数 :param width: 图片宽度 :param height: 图片高度 :param code_length: 验证码字符长度 self.width width self.height height self.code_length code_length # 字符集去除易混淆字符 self.charset string.digits string.ascii_uppercase self.charset self.charset.replace(I, ).replace(O, ).replace(Z, ).replace(0, ).replace(1, ) # 字体路径优先使用项目内的字体文件否则使用默认字体 self.font_path getattr(settings, CAPTCHA_FONT_PATH, None) if not self.font_path or not os.path.exists(self.font_path): # 如果未指定或文件不存在Pillow会回退到默认字体但可能不支持中文或样式固定 self.font None else: self.font ImageFont.truetype(self.font_path, size38) # 字体大小可调 def generate_code(self): 生成随机验证码字符串 return .join(random.choice(self.charset) for _ in range(self.code_length)) def create_image(self, code): 根据验证码字符串创建图片 # 1. 创建画布 - 使用随机浅色背景 bg_color (random.randint(200, 255), random.randint(200, 255), random.randint(200, 255)) image Image.new(RGB, (self.width, self.height), bg_color) draw ImageDraw.Draw(image) # 2. 绘制干扰点噪点 for _ in range(random.randint(100, 200)): # 噪点数量 x random.randint(0, self.width) y random.randint(0, self.height) draw.point((x, y), fill(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))) # 3. 绘制干扰线 for _ in range(random.randint(3, 5)): # 干扰线数量 x1 random.randint(0, self.width // 2) y1 random.randint(0, self.height) x2 random.randint(self.width // 2, self.width) y2 random.randint(0, self.height) draw.line([(x1, y1), (x2, y2)], fill(random.randint(50, 150), random.randint(50, 150), random.randint(50, 150)), widthrandom.randint(1, 2)) # 4. 绘制验证码文字 font_size min(self.height - 10, int(self.width / self.code_length) - 10) if self.font: # 如果指定了字体文件调整大小 self.font ImageFont.truetype(self.font_path, sizefont_size) else: # 使用默认字体 self.font ImageFont.load_default() # 计算总文本宽度并确定起始x坐标使文本居中 if hasattr(self.font, getbbox): # Pillow 9.2.0 total_width sum(self.font.getbbox(char)[2] - self.font.getbbox(char)[0] for char in code) else: # 旧版本兼容 total_width sum(self.font.getsize(char)[0] for char in code) x (self.width - total_width) // 2 for i, char in enumerate(code): # 为每个字符设置随机颜色深色 char_color (random.randint(0, 100), random.randint(0, 100), random.randint(0, 100)) # 每个字符的y坐标随机上下偏移增加扭曲感 y_offset random.randint(-5, 5) y (self.height - font_size) // 2 y_offset # 绘制字符 draw.text((x, y), char, fontself.font, fillchar_color) # 更新下一个字符的x坐标使用字符宽度而非固定间距 if hasattr(self.font, getbbox): char_width self.font.getbbox(char)[2] - self.font.getbbox(char)[0] else: char_width self.font.getsize(char)[0] x char_width random.randint(-2, 2) # 轻微随机间距 # 5. 应用轻微模糊滤镜进一步干扰机器识别可选 image image.filter(ImageFilter.SMOOTH_MORE) # 6. 将图片保存到内存字节流 buffer BytesIO() image.save(buffer, formatPNG) image_data buffer.getvalue() buffer.close() return image_data代码关键点解析字符集优化我们创建了一个去除了I, O, Z, 0, 1等易混淆字符的字符集提升人眼识别率。字体处理提供了自定义字体文件的路径配置通过Django settings的CAPTCHA_FONT_PATH这能显著提升验证码的样式可控性和专业性。如果未指定则回退到Pillow默认字体。背景与干扰背景使用随机浅色干扰点和干扰线使用随机颜色和位置确保每次生成的图片都不同。文字绘制这是最核心的部分。我们让每个字符在垂直方向有随机偏移并且字符间距也是随机的这比简单地水平排列一行字要安全得多。计算字符宽度并动态调整位置确保了无论生成什么字符整体都能大致居中。模糊滤镜ImageFilter.SMOOTH_MORE是一个轻微的模糊效果能平滑锯齿边缘同时给OCR制造一点小麻烦。内存流输出我们使用BytesIO将图片数据保存在内存中而不是写入磁盘这对于Web响应来说效率最高。4.3 生成验证码的Django视图接下来我们需要创建一个视图当用户访问特定URL如/captcha/时生成图片并返回。# yourapp/views.py from django.http import HttpResponse from django.views.decorators.http import require_GET from django.views.decorators.cache import never_cache from .utils.captcha import Captcha require_GET # 确保只响应GET请求 never_cache # 非常重要禁止浏览器缓存验证码图片 def captcha_image(request): 生成验证码图片的视图 # 1. 实例化验证码类并生成代码 captcha_tool Captcha() code captcha_tool.generate_code() # 2. 将验证码代码存入当前会话Session request.session[captcha_code] code # 可选为验证码设置独立的会话键或增加时间戳 # request.session[captcha_code] {code: code, time: time.time()} # 3. 生成图片二进制数据 image_data captcha_tool.create_image(code) # 4. 构建HttpResponse返回图片 response HttpResponse(image_data, content_typeimage/png) # 添加头部确保不被缓存 response[Cache-Control] no-cache, no-store, must-revalidate response[Pragma] no-cache response[Expires] 0 return response视图要点解析never_cache装饰器这是安全关键点。如果验证码图片被浏览器或中间代理缓存了那么同一个图片对应同一个验证码可能会被返回给不同用户或同一用户的不同请求导致验证码失效或被绕过。这个装饰器确保每次请求都会到达服务器端执行生成逻辑。Session存储生成的code被存入request.session。这个session对象在Django中间件处理下会自动保存。响应头设置除了装饰器我们还手动设置了Cache-Control等HTTP头双重保障图片不被缓存。内容类型content_typeimage/png告诉浏览器这是一个PNG图片浏览器会正确渲染它。4.4 前端HTML模板集成在前端模板中我们需要展示这个验证码图片并提供刷新功能。!-- 例如在 login.html 或 register.html 中 -- form methodpost action{% url login %} {% csrf_token %} !-- 其他表单字段如用户名、密码 -- div classform-group label forid_captcha验证码/label div classcaptcha-container img idcaptcha-image src{% url captcha_image %} alt验证码 title点击刷新 stylecursor: pointer; vertical-align: middle; border: 1px solid #ddd; input typetext idid_captcha namecaptcha required maxlength4 placeholder请输入图片中的字符 a hrefjavascript:void(0); onclickrefreshCaptcha(); stylemargin-left: 10px;看不清换一张/a /div small classform-text text-muted请输入图片中的4位字符不区分大小写。/small /div button typesubmit classbtn btn-primary提交/button /form script function refreshCaptcha() { var captchaImage document.getElementById(captcha-image); // 在URL后添加一个随机参数时间戳来强制浏览器重新请求避免缓存 captchaImage.src {% url captcha_image %}?t new Date().getTime(); } // 点击图片本身也可以刷新 document.getElementById(captcha-image).onclick refreshCaptcha; /script前端集成解析img标签src指向我们刚定义的captcha_image视图的URL需要在urls.py中配置见下文。alt和title属性提升可访问性和用户体验。输入框name“captcha”非常重要后端视图将通过这个名称获取用户输入。刷新功能通过一个简单的JavaScript函数refreshCaptcha()实现。其原理是修改图片的src属性在URL末尾添加一个随机的时间戳参数?t...。这能绕过浏览器对同一URL的缓存确保每次点击都会向服务器发起新的GET请求生成新的验证码图片和代码。同时点击图片本身也绑定该函数。用户体验提示通过small标签提示用户验证码的长度和大小写规则。4.5 路由配置与校验视图集成首先在应用的urls.py中配置验证码图片的URL# yourapp/urls.py from django.urls import path from . import views app_name yourapp urlpatterns [ # ... 你的其他URL ... path(captcha/, views.captcha_image, namecaptcha_image), ]然后在需要进行验证码校验的视图如登录视图login_view中加入校验逻辑# yourapp/views.py (续) from django.shortcuts import render, redirect from django.contrib import messages from django.contrib.auth import authenticate, login def login_view(request): if request.method POST: username request.POST.get(username) password request.POST.get(password) user_captcha request.POST.get(captcha, ).strip().upper() # 获取用户输入并转大写 stored_captcha request.session.get(captcha_code, ) # 1. 首先校验验证码 if not stored_captcha: messages.error(request, 验证码已失效请刷新后重试。) elif user_captcha ! stored_captcha: messages.error(request, 验证码错误。) else: # 2. 验证码正确进行用户认证 user authenticate(request, usernameusername, passwordpassword) if user is not None: login(request, user) # 登录成功后清除用过的验证码 if captcha_code in request.session: del request.session[captcha_code] return redirect(home) else: messages.error(request, 用户名或密码错误。) # 3. 无论验证码错误还是密码错误都清除本次验证码要求用户刷新 if captcha_code in request.session: del request.session[captcha_code] # 重新渲染登录页面表单会保留已填写的用户名 return render(request, yourapp/login.html, {username: username}) # GET请求显示空表单 return render(request, yourapp/login.html)校验逻辑要点立即获取并清理Session校验完成后无论对错都立即使用del删除Session中的captcha_code。这实现了验证码的“一次性”防止同一验证码被多次尝试重放攻击。不区分大小写通过.upper()或.lower()将用户输入和存储的代码统一转为大写或小写后再比较提升用户体验。清晰的错误提示区分“验证码失效”Session中不存在、“验证码错误”和“用户密码错误”给用户明确的反馈。流程顺序先校验验证码再校验用户名密码。验证码是成本更低的校验可以提前拦截无效请求减轻认证系统的压力。5. 常见问题与排查技巧实录在实际部署和使用过程中你可能会遇到一些典型问题。下面是我在多次实践中总结出来的排查清单和解决方案。5.1 验证码图片显示为“破损”图标或无法加载这是最常见的问题通常不是Pillow绘图代码的问题而是HTTP响应或前端配置有误。检查点1视图的响应内容类型Content-Type确保你的HttpResponse明确设置了content_typeimage/png。如果设置为text/html或默认浏览器将无法正确解析图片数据。检查点2图片数据是否为有效的PNG二进制流在开发时可以临时修改视图将生成的image_data保存到本地文件用图片查看器打开确认Pillow生成的图片本身是正确的。with open(/tmp/debug_captcha.png, wb) as f: f.write(image_data)检查点3前端img标签的src路径是否正确打开浏览器开发者工具F12切换到“网络”(Network)选项卡刷新页面并查看验证码图片的请求。如果请求状态不是200 OK而是404说明URL配置有误检查urls.py和模板中的{% url %}标签。如果请求状态是200但“预览”(Preview)标签页显示空白或错误点击“响应”(Response)标签看返回的是否是一堆乱码二进制数据如果是则内容类型可能正确但图片数据本身可能有问题回溯到检查点2。如果返回的是HTML代码说明你的视图可能被其他中间件拦截或返回了错误页面。检查点4缓存问题即使你使用了never_cache一些激进的浏览器扩展或公司网络代理仍可能缓存图片。确保你的JavaScript刷新函数?ttimestamp正常工作。可以在网络请求中看到每次请求的URL是否不同。5.2 验证码校验总是失败用户明明输入了看起来正确的字符但系统一直报错。检查点1Session是否正常工作Django的Session中间件django.contrib.sessions.middleware.SessionMiddleware必须位于MIDDLEWARE设置中。同时确保你没有在视图装饰器如login_required之前错误地清空了Session。 可以在生成验证码的视图和校验视图里临时打印request.session.session_key和request.session[‘captcha_code’]看看是否一致且存在。检查点2字符集与大小写处理确认你生成验证码的字符集self.charset和校验时转换大小写的逻辑是匹配的。例如生成时用了大写字母校验时也应该将用户输入转为大写再比较。一个常见的坑是字符集排除了‘0‘和‘O‘但用户可能还是分不清。检查点3验证码的一次性逻辑你的校验视图是否在第一次校验后无论对错就立即删除了Session中的验证码如果是那么用户第一次输错后刷新页面看到的是新验证码但提交时表单里还是旧的输入值这会导致用旧输入去匹配新Session必然失败。解决方案是在每次生成新验证码图片时才覆盖Session中的旧值在校验后立即删除它。同时前端最好能在验证码刷新时清空输入框。function refreshCaptcha() { // ... 刷新图片 ... document.getElementById(id_captcha).value ; // 清空输入框 }检查点4多标签页或浏览器兼容性问题用户在标签页A打开了登录页在标签页B又打开了一个两个页面的验证码不同但Session中只保存了最后一个。当用户在标签页A提交时校验的是B的验证码。这个问题较难彻底解决属于用户体验设计范畴。可以在生成验证码时在Session中用一个更复杂的结构例如存储一个列表或字典键包含时间戳或随机数并将这个随机数作为参数传递给前端图片URL如/captcha/?keyabc123校验时再根据这个key来查找对应的验证码。但这会增加复杂度。对于大多数场景简单的Session存储已足够只需在页面提示用户“验证码已刷新”即可。5.3 性能与高并发考量我们的实现将图片生成放在请求响应周期内对于中小流量网站完全足够。但如果遇到非常高并发的场景如秒杀活动登录页每个请求都动态生成图片会给CPU带来压力。优化思路1缓存生成的验证码文本-图片对可以使用Django的缓存框架如Memcached或Redis。当生成一个验证码时将{‘code’: ‘A3B9’, ‘image_data’: b’...’}存入缓存设置一个很短的过期时间如2分钟。键可以是一个UUID。然后将这个UUID通过Cookie或前端隐藏字段传给客户端。当请求图片时视图根据UUID从缓存中取出图片数据直接返回避免实时生成。校验时也根据这个UUID从缓存取出验证码文本进行比对。这相当于把Session存储换成了更快的缓存存储。优化思路2预生成验证码池在应用启动时或通过一个后台任务预生成一批验证码图片和文本存入缓存或消息队列。当有请求时直接从池子里取一个用用完后标记为已使用或丢弃。这可以将CPU压力从请求高峰期转移到低峰期。注意对于绝大多数项目最初的简单实现完全够用。不要过早优化。只有当监控显示验证码生成视图确实成为瓶颈时再考虑上述方案。5.4 关于字体文件的坑使用自定义字体.ttf能让验证码更美观、更专业但也是踩坑高发区。字体文件路径推荐将字体文件放在项目的static目录或某个应用下的static子目录中然后在settings.py中配置路径。# settings.py import os BASE_DIR os.path.dirname(os.path.dirname(os.path.abspath(__file__))) CAPTCHA_FONT_PATH os.path.join(BASE_DIR, yourapp, static, fonts, arial.ttf)确保部署时静态文件收集命令python manage.py collectstatic能正确收集到该字体文件。“No such file or directory”错误这是部署时最常见的问题。在开发环境路径正确但部署到生产服务器如Nginx uWSGI后当前工作目录可能变化导致相对路径失效。务必使用绝对路径如上例所示基于BASE_DIR构建。字体文件权限确保运行Django进程的用户如www-data,nginx有读取字体文件的权限。中文字符支持如果你的验证码需要包含中文务必选择一个包含中文字符的字体文件如.ttc或支持中文的.ttf。Pillow的默认字体通常不支持中文使用中文字符集而不指定中文字体会导致绘制失败或显示乱码。通过以上从设计到实现再到问题排查的完整流程你已经掌握了一个健壮、可定制、不依赖第三方库的Django验证码解决方案。这个方案不仅能够直接用于你的项目其背后关于安全、用户体验和性能的思考也能应用到Web开发的许多其他场景中。