查看: 1248|回复: 3

Python验证码生成与Django/Flask校验及安全防护

[复制链接]
发表于 昨天 10:00 | 显示全部楼层 |阅读模式
验证码的生成与校验通常横跨图片渲染、会话状态、缓存和限流几个环节。Python 生态里既有轻量库,也有框架专用扩展,还可以直接用 Pillow 做底层定制;高安全场景则会接入商业验证码 API。选型时应重点看依赖数量、字体文件是否可控、验证码是否一次性失效,以及校验失败后的计数与过期策略。

一、验证码库选型与关键参数

captcha 库适合作为轻量级首选方案,纯 Python 实现,安装命令为:
  1. pip install captcha
复制代码
生成数字验证码的典型调用如下:
  1. from captcha.image import ImageCaptcha
  2. image = ImageCaptcha(width=300, height=100)
  3. data = image.generate('1234')
  4. image.write('1234', 'captcha.png')
复制代码
实际使用中,width 和 height 要跟前端展示区域匹配;fonts 应传入多个系统真实存在的 TTF 字体路径;font_sizes 用于控制字符大小波动。原文特别提示,默认字体可能出现显示异常,建议通过 fonts 参数指定字体文件,这是图片验证码部署时很常见的兼容性问题。

django-simple-captcha 是 Django 生态中的专用方案,特点是能接入 Django admin 后台,提供模型字段和表单字段,并内置数字、字母、算术等多种验证码样式。安装命令为:
  1. pip install django-simple-captcha
复制代码
在 settings.py 中注册应用:
  1. INSTALLED_APPS = [
  2.     ...,
  3.     'captcha',
  4. ]
复制代码
表单中可以直接使用 CaptchaField:
  1. from captcha.fields import CaptchaField
  2. class ContactForm(forms.Form):
  3.     captcha = CaptchaField()
复制代码
这种方式减少了手写图片生成和校验逻辑的工作量,适合以 Django 表单为中心的页面。

需要深度定制时,可以直接使用 Pillow 绘制文字和干扰线。原文给出的示例先生成白色背景图片,再加载字体、绘制文本,并随机画 5 条干扰线:
  1. from PIL import Image, ImageDraw, ImageFont
  2. import random
  3. def generate_captcha(text):
  4.     image = Image.new('RGB', (200, 100), color=(255, 255, 255))
  5.     draw = ImageDraw.Draw(image)
  6.     try:
  7.         font = ImageFont.truetype('arial.ttf', 36)
  8.     except:
  9.         font = ImageFont.load_default()
  10.     draw.text((10, 30), text, font=font, fill=(0, 0, 0))
  11.     for _ in range(5):
  12.         x1 = random.randint(0, 200)
  13.         y1 = random.randint(0, 100)
  14.         x2 = random.randint(0, 200)
  15.         y2 = random.randint(0, 100)
  16.         draw.line([(x1, y1), (x2, y2)], fill=(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)), width=2)
  17.     return image
复制代码
这段代码的价值在于可控:字体、文本位置、干扰线数量都可以按业务调整;但字体加载失败时的兜底字体通常不适合正式环境,生产部署应保证字体路径有效。

对安全要求更高的业务,可以评估商业 API。原文列出的对比信息包括:腾讯云验证码免费额度 1000 次/月,识别率 99.8%,按量付费,支持行为式验证和无感验证;阿里云验证码免费额度 500 次/月,识别率 99.5%,套餐包计费,支持滑动拼图和智能风险控制;Geetest 免费额度 100 次/天,识别率 99.9%,阶梯定价,支持 3D 旋转验证和一键验证。典型服务端校验方式如下:
  1. import requests
  2. def verify_captcha(token):
  3.     url = 'https://api.xxx.com/verify'
  4.     params = {
  5.         'secret_key': 'YOUR_KEY',
  6.         'token': token,
  7.         'ip': 'CLIENT_IP'
  8.     }
  9.     response = requests.get(url, params=params)
  10.     return response.json()['success']
复制代码
这类接口通常把 secret_key、token 和客户端 IP 传给服务端,由服务端判断验证结果,不能只依赖前端返回的成功标记。

二、服务端校验与攻击防护

原文引用 OWASP 统计,不安全的验证码主要面临暴力破解、机器学习识别、前端绕过等威胁,其中暴力破解占比 43%,机器学习识别占比 31%,前端绕过占比 18%。对应的落地重点是:验证码一次性使用、失败次数限制、服务端缓存答案、比较时忽略大小写但不忽略空值。Django 中可借助 cache 实现:
  1. from django.core.cache import cache
  2. from django.http import JsonResponse
  3. def verify_captcha(request):
  4.     user_input = request.POST.get('captcha')
  5.     session_id = request.COOKIES.get('sessionid')
  6.     correct_code = cache.get(f'captcha_{session_id}')
  7.     cache.delete(f'captcha_{session_id}')
  8.     if not user_input or not correct_code:
  9.         return JsonResponse({'success': False, 'error': '无效请求'})
  10.     if user_input.lower() != correct_code.lower():
  11.         fails = cache.get(f'captcha_fails_{session_id}', 0) + 1
  12.         cache.set(f'captcha_fails_{session_id}', fails, timeout=300)
  13.         if fails > 3:
  14.             return JsonResponse({'success': False, 'error': '尝试次数过多'})
  15.         return JsonResponse({'success': False, 'error': '验证码错误'})
  16.     return JsonResponse({'success': True})
复制代码
这里有两个关键动作:先取出正确答案,再立即删除缓存,保证同一个验证码不能被重复使用;失败次数写入缓存并设置 300 秒过期,超过 3 次后触发额外限制。比较前把用户输入和正确答案统一转小写,可以避免大小写造成的误判。

前端侧可以增加刷新次数限制,避免用户或脚本无限刷新验证码:
  1. let refreshCount = 0;
  2. function refreshCaptcha() {
  3.     if(refreshCount > 5) {
  4.         alert('刷新次数过多,请稍后再试');
  5.         return;
  6.     }
  7.     document.getElementById('captcha-img').src = '/captcha/?' + Date.now();
  8.     refreshCount++;
  9. }
复制代码
这里的 Date.now() 用于打破浏览器缓存,让图片请求真正到达服务端。

三、Django 集成流程

在 settings.py 中可以集中配置验证码参数,例如字体大小、图片尺寸、验证码长度、超时时间以及干扰函数:
  1. CAPTCHA_FONT_SIZE = 36
  2. CAPTCHA_IMAGE_SIZE = (120, 60)
  3. CAPTCHA_LENGTH = 6
  4. CAPTCHA_TIMEOUT = 300
  5. CAPTCHA_NOISE_FUNCTIONS = (
  6.     'captcha.helpers.noise_arcs',
  7.     'captcha.helpers.noise_dots',
  8. )
复制代码
如果使用 captcha 库自行生成图片,可以在视图里生成随机码、写入 session,再返回 PNG 响应:
  1. from django.http import HttpResponse
  2. from django.views.decorators.http import require_GET
  3. from captcha.image import ImageCaptcha
  4. import random
  5. @require_GET
  6. def generate_captcha(request):
  7.     text = ''.join(random.choices('ABCDEFGHJKLMNPQRSTUVWXYZ23456789', k=6))
  8.     request.session['captcha'] = text
  9.     request.session.set_expiry(300)
  10.     image = ImageCaptcha(width=300, height=100)
  11.     data = image.generate(text)
  12.     return HttpResponse(data, content_type='image/png')
复制代码
字符集里去掉了容易混淆的字符,session 过期时间设为 300 秒。模板中把图片和输入框放在同一个表单内:
  1. <form method='post'>
  2.     {% csrf_token %}
  3.     <div class='form-group'>
  4.         <label>验证码:</label>
  5.         <img src='{% url 'captcha' %}'>
  6.         <input type='text' name='captcha' required>
  7.     </div>
  8.     <button type='submit'>提交</button>
  9. </form>
复制代码
点击图片刷新时,可以在前端拼接时间戳重新请求同一个图片地址。

四、Flask 集成方案

Flask 的基础实现同样是把随机码写入 session,再返回图片内容:
  1. from flask import Flask, session, request, make_response
  2. import random
  3. from captcha.image import ImageCaptcha
  4. app = Flask(__name__)
  5. app.secret_key = 'your_secret_key'
  6. @app.route('/captcha')
  7. def get_captcha():
  8.     text = ''.join(random.choices('23456789ABCDEFGHJKLMNPQRSTUVWXYZ', k=4))
  9.     session['captcha'] = text
  10.     image = ImageCaptcha(width=280, height=90)
  11.     data = image.generate(text)
  12.     response = make_response(data)
  13.     response.headers['Content-Type'] = 'image/png'
  14.     return response
  15. @app.route('/verify', methods=['POST'])
  16. def verify():
  17.     user_input = request.form.get('captcha', '').upper()
  18.     if 'captcha' not in session:
  19.         return '验证码已过期', 400
  20.     if user_input == session['captcha']:
  21.         session.pop('captcha')
  22.         return '验证成功'
  23.     else:
  24.         return '验证码错误', 400
复制代码
验证成功后调用 session.pop 删除验证码,避免重复提交。使用 Flask-Captcha2 扩展时,可以通过配置项控制长度、宽度和高度,并用装饰器保护视图:
  1. from flask_captcha2 import FlaskCaptcha
  2. app = Flask(__name__)
  3. app.config['CAPTCHA_ENABLE'] = True
  4. app.config['CAPTCHA_LENGTH'] = 5
  5. app.config['CAPTCHA_WIDTH'] = 200
  6. app.config['CAPTCHA_HEIGHT'] = 100
  7. captcha = FlaskCaptcha(app)
  8. @app.route('/protected')
  9. @captcha.required
  10. def protected_view():
  11.     return '验证通过'
复制代码
这种方式适合需要快速给多个路由增加验证码校验的场景。

五、性能优化与疑难排查

高并发场景下,验证码答案应放入可靠的共享缓存。原文给出 Redis Cluster 和多级缓存配置示例:
  1. import redis
  2. redis_client = redis.RedisCluster(
  3.     startup_nodes=[
  4.         {'host': 'redis1', 'port': 6379},
  5.         {'host': 'redis2', 'port': 6379}
  6.     ],
  7.     decode_responses=True
  8. )
  9. CACHES = {
  10.     'default': {
  11.         'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
  12.         'LOCATION': 'memcached:11211',
  13.     },
  14.     'redis': {
  15.         'BACKEND': 'django_redis.cache.RedisCache',
  16.         'LOCATION': 'redis://redis:6379/1',
  17.         'OPTIONS': {
  18.             'CLIENT_CLASS': 'django_redis.client.DefaultClient',
  19.         }
  20.     }
  21. }
复制代码
当图片生成成为瓶颈时,可以用 Celery 把生成任务异步化,并将图片数据以 base64 返回:
  1. from celery import Celery
  2. from captcha.image import ImageCaptcha
  3. import base64
  4. app = Celery('tasks', broker='pyamqp://guest@localhost//')
  5. @app.task
  6. def generate_captcha_async(text):
  7.     image = ImageCaptcha()
  8.     data = image.generate(text)
  9.     return base64.b64encode(data.getvalue()).decode('utf-8')
复制代码
常见问题方面,字体显示异常通常要检查字体文件路径是否正确,原文建议使用绝对路径并检查文件权限:
  1. font_path = os.path.join(os.path.dirname(__file__), 'fonts/arial.ttf')
  2. image = ImageCaptcha(fonts=[font_path])
复制代码
验证码图片不刷新时,优先检查浏览器缓存和 session,再让前端请求带上时间戳。性能瓶颈可以用 cProfile 定位:
  1. import cProfile
  2. def generate_test():
  3.     for _ in range(1000):
  4.         image = ImageCaptcha()
  5.         image.generate('test')
  6. cProfile.run('generate_test()', sort='cumtime')
复制代码
原文给出的典型优化点包括字体预加载、图片生成复用和缓存验证码结果。排查时应先区分耗时来自字体加载、图片绘制还是缓存访问。

六、高级方案与演进方向

行为验证码可以通过鼠标轨迹计算速度、速度方差和加速度变化,再交给风控模型评估:
  1. def analyze_behavior(track_data):
  2.     speeds = []
  3.     for i in range(1, len(track_data)):
  4.         dx = track_data[i]['x'] - track_data[i-1]['x']
  5.         dy = track_data[i]['y'] - track_data[i-1]['y']
  6.         dt = track_data[i]['t'] - track_data[i-1]['t']
  7.         speed = (dx**2 + dy**2)**0.5 / dt if dt > 0 else 0
  8.         speeds.append(speed)
  9.     acceleration = np.std([speeds[i]-speeds[i-1] for i in range(1,len(speeds))])
  10.     return {
  11.         'avg_speed': np.mean(speeds),
  12.         'speed_variance': np.var(speeds),
  13.         'acceleration': acceleration
  14.     }
复制代码
深度学习防护可以用 CNN 识别自动化工具特征,原文给出的模型结构是两层卷积、两层池化,再经全连接层输出 sigmoid 概率:
  1. import tensorflow as tf
  2. from tensorflow.keras import layers
  3. def build_detection_model():
  4.     model = tf.keras.Sequential([
  5.         layers.Conv2D(32, (3,3), activation='relu', input_shape=(100, 300, 3)),
  6.         layers.MaxPooling2D((2,2)),
  7.         layers.Conv2D(64, (3,3), activation='relu'),
  8.         layers.MaxPooling2D((2,2)),
  9.         layers.Flatten(),
  10.         layers.Dense(64, activation='relu'),
  11.         layers.Dense(1, activation='sigmoid')
  12.     ])
  13.     model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
  14.     return model
复制代码
多因素验证可以把传统验证码、行为分析和设备指纹组合起来,按平均分或阈值判断是否通过:
  1. class MultiFactorAuth:
  2.     def __init__(self):
  3.         self.factors = [
  4.             self.captcha_check,
  5.             self.behavior_analysis,
  6.             self.device_fingerprint
  7.         ]
  8.     def verify(self, request):
  9.         scores = []
  10.         for factor in self.factors:
  11.             scores.append(factor(request))
  12.         total_score = sum(scores) / len(scores)
  13.         return total_score > 0.7
  14.     def captcha_check(self, request):
  15.         pass
  16.     def behavior_analysis(self, request):
  17.         pass
  18.     def device_fingerprint(self, request):
  19.         pass
复制代码
原文还提到可用 GAN 生成更复杂的验证码样本,并给出了生成器网络结构,包括 Dense、Reshape、Conv2DTranspose 等层。对普通业务来说,更重要的是把验证码强度与风险等级匹配:登录、评论等低风险入口可用简单图形验证码加失败计数,金融交易等高风险管理入口则适合多因素验证与高强度图形验证码组合,避免安全措施拖累正常用户体验。
回复

使用道具 举报

发表于 昨天 19:00 | 显示全部楼层

Re: Python验证码生成与Django/Flask校验及安全防护

感谢整理,这篇把从轻量库到框架扩展再到商业 API 的选型路径讲得挺清楚,尤其是字体路径这个容易踩的坑,我部署时也遇到过,默认字体在部分系统上直接渲染成方块,后来把 TTF 文件打包进项目、用绝对路径指定才稳定下来。 有几点想补充和交流:一是楼主最后那段校验代码好像截断了,失败计数的上限和锁定时间想请教下具体策略,是超过 N 次就暂时拒绝并返回统一提示,还是直接拉长下次验证的冷却时间?另外失败计数本身也建议设置过期时间,否则缓存键会越积越多。二是用 sessionid 作为缓存键有个隐患,未登录用户或会话重建后可能对不上,最好在生成验证码时下发一个独立的 captcha_id,校验和删除都用它。三是商业 API 的服务端校验除了 token 和 IP,通常还会带时间戳或签名,secret 一定要只放服务端,前端拿到的成功标记不能作为放行依据。四是对付机器学习识别,光靠随机干扰线效果有限,加一点字符旋转、扭曲和噪点会更有用,当然可读性也要兼顾。整体思路很实用,收藏了。
回复 支持 反对

使用道具 举报

发表于 昨天 19:10 | 显示全部楼层

Re: Python验证码生成与Django/Flask校验及安全防护

感谢分享,这块内容整理得比较系统,尤其是把“生成—存储—校验—失效”拆开来讲,比单纯贴一段生成验证码的代码实用很多。 几点体会:字体确实是最容易被忽略的坑。我这边用 Pillow 时也遇到过默认字体在容器里渲染成方块的情况,后来改成把 ttf 文件一起打进镜像、用绝对路径传入,并且在启动时做一次字体可加载性检查,比依赖系统字体稳。干扰线数量也得克制,五条以内还能看清,再多一些用户就会频繁输错,反而增加失败计数。 关于一次性失效和失败计数,首帖里 cache 删除答案、再按 session 累计失败次数的思路很关键,实际落地时可以再加上过期时间到点自动失效,以及失败次数达到阈值后短暂锁定,这样能同时防住暴力尝试和脚本重放。比较时注意空值判断这点也很重要,很多绕过都是拿空字符串去比。 选型上我比较认同按场景分层:Django 表单为主的页面直接用 django-simple-captcha 省事;需要自己控制外观和干扰元素就用 Pillow;有风控要求的业务再评估商业 API,但要记住校验必须服务端发起,前端返回的成功标记不能作数。 另外首帖末尾失败计数往后的限流部分似乎还没展开,如果方便的话,挺想看看按 IP 或 session 做滑动窗口限流的具体做法。
回复 支持 反对

使用道具 举报

发表于 昨天 19:20 | 显示全部楼层

Re: Python验证码生成与Django/Flask校验及安全防护

这篇整理得很系统,把验证码从图片生成、会话缓存、一次性失效到失败限流几个层面拆开了,选型时确实应该先看依赖、字体可控性和过期策略,而不是只比较库名。 我比较认同里面强调的“服务端缓存答案”和“不能只信前端成功标记”,这两点在 Django、Flask 落地时都很关键。django-simple-captcha 适合表单场景,Pillow 示例则适合需要自定义干扰线和样式的需求,但生产环境一定要处理好字体路径,不能让默认兜底字体成为正式方案。 另外首帖末尾的 Django 校验函数好像还没贴完,后续如果能补上失败次数自增、过期时间和达到阈值后的封禁或冷却策略就更完整了。用 sessionid 做缓存键时,也可以顺带考虑未登录、会话重建这些边界情况。整体对做验证码选型和防护设计挺有参考价值。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-23 13:49 , Processed in 0.025179 second(s), 28 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部