验证码的生成与校验通常横跨图片渲染、会话状态、缓存和限流几个环节。Python 生态里既有轻量库,也有框架专用扩展,还可以直接用 Pillow 做底层定制;高安全场景则会接入商业验证码 API。选型时应重点看依赖数量、字体文件是否可控、验证码是否一次性失效,以及校验失败后的计数与过期策略。
一、验证码库选型与关键参数
captcha 库适合作为轻量级首选方案,纯 Python 实现,安装命令为:生成数字验证码的典型调用如下:- from captcha.image import ImageCaptcha
- image = ImageCaptcha(width=300, height=100)
- data = image.generate('1234')
- image.write('1234', 'captcha.png')
复制代码 实际使用中,width 和 height 要跟前端展示区域匹配;fonts 应传入多个系统真实存在的 TTF 字体路径;font_sizes 用于控制字符大小波动。原文特别提示,默认字体可能出现显示异常,建议通过 fonts 参数指定字体文件,这是图片验证码部署时很常见的兼容性问题。
django-simple-captcha 是 Django 生态中的专用方案,特点是能接入 Django admin 后台,提供模型字段和表单字段,并内置数字、字母、算术等多种验证码样式。安装命令为:- pip install django-simple-captcha
复制代码 在 settings.py 中注册应用:- INSTALLED_APPS = [
- ...,
- 'captcha',
- ]
复制代码 表单中可以直接使用 CaptchaField:- from captcha.fields import CaptchaField
- class ContactForm(forms.Form):
- captcha = CaptchaField()
复制代码 这种方式减少了手写图片生成和校验逻辑的工作量,适合以 Django 表单为中心的页面。
需要深度定制时,可以直接使用 Pillow 绘制文字和干扰线。原文给出的示例先生成白色背景图片,再加载字体、绘制文本,并随机画 5 条干扰线:- from PIL import Image, ImageDraw, ImageFont
- import random
- def generate_captcha(text):
- image = Image.new('RGB', (200, 100), color=(255, 255, 255))
- draw = ImageDraw.Draw(image)
- try:
- font = ImageFont.truetype('arial.ttf', 36)
- except:
- font = ImageFont.load_default()
- draw.text((10, 30), text, font=font, fill=(0, 0, 0))
- for _ in range(5):
- x1 = random.randint(0, 200)
- y1 = random.randint(0, 100)
- x2 = random.randint(0, 200)
- y2 = random.randint(0, 100)
- draw.line([(x1, y1), (x2, y2)], fill=(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)), width=2)
- return image
复制代码 这段代码的价值在于可控:字体、文本位置、干扰线数量都可以按业务调整;但字体加载失败时的兜底字体通常不适合正式环境,生产部署应保证字体路径有效。
对安全要求更高的业务,可以评估商业 API。原文列出的对比信息包括:腾讯云验证码免费额度 1000 次/月,识别率 99.8%,按量付费,支持行为式验证和无感验证;阿里云验证码免费额度 500 次/月,识别率 99.5%,套餐包计费,支持滑动拼图和智能风险控制;Geetest 免费额度 100 次/天,识别率 99.9%,阶梯定价,支持 3D 旋转验证和一键验证。典型服务端校验方式如下:- import requests
- def verify_captcha(token):
- url = 'https://api.xxx.com/verify'
- params = {
- 'secret_key': 'YOUR_KEY',
- 'token': token,
- 'ip': 'CLIENT_IP'
- }
- response = requests.get(url, params=params)
- return response.json()['success']
复制代码 这类接口通常把 secret_key、token 和客户端 IP 传给服务端,由服务端判断验证结果,不能只依赖前端返回的成功标记。
二、服务端校验与攻击防护
原文引用 OWASP 统计,不安全的验证码主要面临暴力破解、机器学习识别、前端绕过等威胁,其中暴力破解占比 43%,机器学习识别占比 31%,前端绕过占比 18%。对应的落地重点是:验证码一次性使用、失败次数限制、服务端缓存答案、比较时忽略大小写但不忽略空值。Django 中可借助 cache 实现:- from django.core.cache import cache
- from django.http import JsonResponse
- def verify_captcha(request):
- user_input = request.POST.get('captcha')
- session_id = request.COOKIES.get('sessionid')
- correct_code = cache.get(f'captcha_{session_id}')
- cache.delete(f'captcha_{session_id}')
- if not user_input or not correct_code:
- return JsonResponse({'success': False, 'error': '无效请求'})
- if user_input.lower() != correct_code.lower():
- fails = cache.get(f'captcha_fails_{session_id}', 0) + 1
- cache.set(f'captcha_fails_{session_id}', fails, timeout=300)
- if fails > 3:
- return JsonResponse({'success': False, 'error': '尝试次数过多'})
- return JsonResponse({'success': False, 'error': '验证码错误'})
- return JsonResponse({'success': True})
复制代码 这里有两个关键动作:先取出正确答案,再立即删除缓存,保证同一个验证码不能被重复使用;失败次数写入缓存并设置 300 秒过期,超过 3 次后触发额外限制。比较前把用户输入和正确答案统一转小写,可以避免大小写造成的误判。
前端侧可以增加刷新次数限制,避免用户或脚本无限刷新验证码:- let refreshCount = 0;
- function refreshCaptcha() {
- if(refreshCount > 5) {
- alert('刷新次数过多,请稍后再试');
- return;
- }
- document.getElementById('captcha-img').src = '/captcha/?' + Date.now();
- refreshCount++;
- }
复制代码 这里的 Date.now() 用于打破浏览器缓存,让图片请求真正到达服务端。
三、Django 集成流程
在 settings.py 中可以集中配置验证码参数,例如字体大小、图片尺寸、验证码长度、超时时间以及干扰函数:- CAPTCHA_FONT_SIZE = 36
- CAPTCHA_IMAGE_SIZE = (120, 60)
- CAPTCHA_LENGTH = 6
- CAPTCHA_TIMEOUT = 300
- CAPTCHA_NOISE_FUNCTIONS = (
- 'captcha.helpers.noise_arcs',
- 'captcha.helpers.noise_dots',
- )
复制代码 如果使用 captcha 库自行生成图片,可以在视图里生成随机码、写入 session,再返回 PNG 响应:- from django.http import HttpResponse
- from django.views.decorators.http import require_GET
- from captcha.image import ImageCaptcha
- import random
- @require_GET
- def generate_captcha(request):
- text = ''.join(random.choices('ABCDEFGHJKLMNPQRSTUVWXYZ23456789', k=6))
- request.session['captcha'] = text
- request.session.set_expiry(300)
- image = ImageCaptcha(width=300, height=100)
- data = image.generate(text)
- return HttpResponse(data, content_type='image/png')
复制代码 字符集里去掉了容易混淆的字符,session 过期时间设为 300 秒。模板中把图片和输入框放在同一个表单内:- <form method='post'>
- {% csrf_token %}
- <div class='form-group'>
- <label>验证码:</label>
- <img src='{% url 'captcha' %}'>
- <input type='text' name='captcha' required>
- </div>
- <button type='submit'>提交</button>
- </form>
复制代码 点击图片刷新时,可以在前端拼接时间戳重新请求同一个图片地址。
四、Flask 集成方案
Flask 的基础实现同样是把随机码写入 session,再返回图片内容:- from flask import Flask, session, request, make_response
- import random
- from captcha.image import ImageCaptcha
- app = Flask(__name__)
- app.secret_key = 'your_secret_key'
- @app.route('/captcha')
- def get_captcha():
- text = ''.join(random.choices('23456789ABCDEFGHJKLMNPQRSTUVWXYZ', k=4))
- session['captcha'] = text
- image = ImageCaptcha(width=280, height=90)
- data = image.generate(text)
- response = make_response(data)
- response.headers['Content-Type'] = 'image/png'
- return response
- @app.route('/verify', methods=['POST'])
- def verify():
- user_input = request.form.get('captcha', '').upper()
- if 'captcha' not in session:
- return '验证码已过期', 400
- if user_input == session['captcha']:
- session.pop('captcha')
- return '验证成功'
- else:
- return '验证码错误', 400
复制代码 验证成功后调用 session.pop 删除验证码,避免重复提交。使用 Flask-Captcha2 扩展时,可以通过配置项控制长度、宽度和高度,并用装饰器保护视图:- from flask_captcha2 import FlaskCaptcha
- app = Flask(__name__)
- app.config['CAPTCHA_ENABLE'] = True
- app.config['CAPTCHA_LENGTH'] = 5
- app.config['CAPTCHA_WIDTH'] = 200
- app.config['CAPTCHA_HEIGHT'] = 100
- captcha = FlaskCaptcha(app)
- @app.route('/protected')
- @captcha.required
- def protected_view():
- return '验证通过'
复制代码 这种方式适合需要快速给多个路由增加验证码校验的场景。
五、性能优化与疑难排查
高并发场景下,验证码答案应放入可靠的共享缓存。原文给出 Redis Cluster 和多级缓存配置示例:- import redis
- redis_client = redis.RedisCluster(
- startup_nodes=[
- {'host': 'redis1', 'port': 6379},
- {'host': 'redis2', 'port': 6379}
- ],
- decode_responses=True
- )
- CACHES = {
- 'default': {
- 'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
- 'LOCATION': 'memcached:11211',
- },
- 'redis': {
- 'BACKEND': 'django_redis.cache.RedisCache',
- 'LOCATION': 'redis://redis:6379/1',
- 'OPTIONS': {
- 'CLIENT_CLASS': 'django_redis.client.DefaultClient',
- }
- }
- }
复制代码 当图片生成成为瓶颈时,可以用 Celery 把生成任务异步化,并将图片数据以 base64 返回:- from celery import Celery
- from captcha.image import ImageCaptcha
- import base64
- app = Celery('tasks', broker='pyamqp://guest@localhost//')
- @app.task
- def generate_captcha_async(text):
- image = ImageCaptcha()
- data = image.generate(text)
- return base64.b64encode(data.getvalue()).decode('utf-8')
复制代码 常见问题方面,字体显示异常通常要检查字体文件路径是否正确,原文建议使用绝对路径并检查文件权限:- font_path = os.path.join(os.path.dirname(__file__), 'fonts/arial.ttf')
- image = ImageCaptcha(fonts=[font_path])
复制代码 验证码图片不刷新时,优先检查浏览器缓存和 session,再让前端请求带上时间戳。性能瓶颈可以用 cProfile 定位:- import cProfile
- def generate_test():
- for _ in range(1000):
- image = ImageCaptcha()
- image.generate('test')
- cProfile.run('generate_test()', sort='cumtime')
复制代码 原文给出的典型优化点包括字体预加载、图片生成复用和缓存验证码结果。排查时应先区分耗时来自字体加载、图片绘制还是缓存访问。
六、高级方案与演进方向
行为验证码可以通过鼠标轨迹计算速度、速度方差和加速度变化,再交给风控模型评估:- def analyze_behavior(track_data):
- speeds = []
- for i in range(1, len(track_data)):
- dx = track_data[i]['x'] - track_data[i-1]['x']
- dy = track_data[i]['y'] - track_data[i-1]['y']
- dt = track_data[i]['t'] - track_data[i-1]['t']
- speed = (dx**2 + dy**2)**0.5 / dt if dt > 0 else 0
- speeds.append(speed)
- acceleration = np.std([speeds[i]-speeds[i-1] for i in range(1,len(speeds))])
- return {
- 'avg_speed': np.mean(speeds),
- 'speed_variance': np.var(speeds),
- 'acceleration': acceleration
- }
复制代码 深度学习防护可以用 CNN 识别自动化工具特征,原文给出的模型结构是两层卷积、两层池化,再经全连接层输出 sigmoid 概率:- import tensorflow as tf
- from tensorflow.keras import layers
- def build_detection_model():
- model = tf.keras.Sequential([
- layers.Conv2D(32, (3,3), activation='relu', input_shape=(100, 300, 3)),
- layers.MaxPooling2D((2,2)),
- layers.Conv2D(64, (3,3), activation='relu'),
- layers.MaxPooling2D((2,2)),
- layers.Flatten(),
- layers.Dense(64, activation='relu'),
- layers.Dense(1, activation='sigmoid')
- ])
- model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
- return model
复制代码 多因素验证可以把传统验证码、行为分析和设备指纹组合起来,按平均分或阈值判断是否通过:- class MultiFactorAuth:
- def __init__(self):
- self.factors = [
- self.captcha_check,
- self.behavior_analysis,
- self.device_fingerprint
- ]
- def verify(self, request):
- scores = []
- for factor in self.factors:
- scores.append(factor(request))
- total_score = sum(scores) / len(scores)
- return total_score > 0.7
- def captcha_check(self, request):
- pass
- def behavior_analysis(self, request):
- pass
- def device_fingerprint(self, request):
- pass
复制代码 原文还提到可用 GAN 生成更复杂的验证码样本,并给出了生成器网络结构,包括 Dense、Reshape、Conv2DTranspose 等层。对普通业务来说,更重要的是把验证码强度与风险等级匹配:登录、评论等低风险入口可用简单图形验证码加失败计数,金融交易等高风险管理入口则适合多因素验证与高强度图形验证码组合,避免安全措施拖累正常用户体验。 |