Python进阶探索20-Python生成验证码
20、Python 教程 - 使用python生成验证码

文章目录
一、痛点场景:没有验证码的网站到底有多惨?
想象一下这个场景:你熬了三个通宵做了一个注册送优惠券的活动,上线第一天信心满满地等着用户涌入。结果第二天早上打开后台一看,注册量暴涨十万,优惠券被领光了,但真实用户一个都没有——全是脚本批量注册的机器人。你的营销预算打了水漂,服务器还被这些垃圾请求拖到宕机。
这就是没有验证码的真实代价。

在实际开发中,我们经常遇到这些让人头疼的问题:
- 注册接口被刷爆:恶意脚本一秒钟注册上百个账号,用来发垃圾广告、薅羊毛
- 短信接口被轰炸:攻击者利用你的短信验证码接口,给任意手机号发垃圾短信,你的短信账单瞬间爆炸
- 登录接口被暴力破解:黑客用字典不断尝试密码,没有验证码的话几小时就能破解弱密码
- 投票活动被操纵:选秀投票、最佳评选,脚本一秒投一千票,真实用户的声音被淹没
- 评论区被垃圾信息淹没:自动发帖机器人到处发广告链接,你的社区变成垃圾场
- 爬虫疯狂抓取数据:你的核心数据被竞争对手一键爬走,带宽成本飙升
我曾经接手过一个项目,上线初期没有加验证码,上线第三天短信接口就被人盯上了,一晚上发了三万条短信,损失了将近两万块钱的短信费。加上之后,攻击量直接下降了百分之九十九。
所以,验证码不是锦上添花的功能,而是Web应用的安全底线。
二、痛点的解决方案:验证码系统的完整架构
那么验证码到底是怎么解决这些问题的呢?我们来看一个完整的验证码系统架构:

整个流程分为六步:
- 用户请求验证码:浏览器向服务器发起获取验证码的请求
- 服务器生成验证码:服务器生成随机字符串,用图像处理库绘制成图片
- 缓存验证码:把验证码文本存入Redis或Session,设置过期时间(通常5分钟)
- 返回图片给用户:把生成的图片返回给前端展示
- 用户提交验证码:用户输入看到的字符,连同表单一起提交
- 服务器校验:从缓存中取出验证码,和用户输入比对,校验通过后立即删除(防止重复使用)
这里有几个关键的安全细节:
- 验证码必须一次性使用,校验通过后立刻删除,不能重复校验
- 必须设置过期时间,防止验证码被暴力枚举
- 验证码比对时建议忽略大小写,提升用户体验
- 生产环境建议用Redis存储,支持分布式部署
三、验证码是什么?
3.1 专业解释
验证码(CAPTCHA)是"Completely Automated Public Turing test to tell Computers and Humans Apart"的缩写,翻译过来就是"全自动区分计算机和人类的图灵测试"。
说人话就是:出一道人类很容易做对、但机器很难做对的题,来判断对面坐的是人还是机器人。
3.2 大白话解释
你可以把验证码理解成网站门口的保安。每个人进来的时候,保安都会问一个问题:“请读出图片上这几个歪歪扭扭的字”。人类一眼就能看出来,但机器人(脚本)识别起来就很困难。这样就把大部分自动化攻击挡在了门外。
3.3 生活案例
就像你去银行办业务,大堂经理会先问你"请问您要办理什么业务?",这个简单的对话就能区分出真正要办业务的客户和发传单的推销员。验证码做的是同样的事情——用一个简单的测试,过滤掉不是真实用户的访问者。
四、为什么要用验证码?
4.1 安全层面
- 防暴力破解:没有验证码,攻击者可以无限次尝试登录密码;有了验证码,每试一次都要重新识别,攻击成本指数级上升
- 防批量注册:阻止脚本批量创建虚假账号
- 防接口滥用:保护短信、邮件等付费接口不被恶意调用
- 防爬虫抓取:增加数据采集的难度,保护核心业务数据
4.2 业务层面
- 保证活动公平:投票、抽奖、秒杀等活动中,防止机器人刷奖
- 维护社区质量:论坛、评论区防止垃圾广告刷屏
- 降低运营成本:减少垃圾账号的审核和清理工作量
4.3 成本对比
我们算一笔账:一条短信验证码大约3到5分钱。如果被攻击者利用,一小时发一万条就是三五百块,一天下来就是上万块。而花半小时写一个图形验证码,就能挡住百分之九十九的这类攻击。这笔账怎么算都划算。
五、验证码是怎么演进过来的?
验证码技术发展到今天,已经经历了四代演进。每一代的出现,都是因为上一代被AI技术攻破了。

5.1 第一代:文本验证码(1997年开始)
1997年,AltaVista搜索引擎为了防止自动提交URL的脚本,发明了最早的文本验证码。就是我们最常见的那种——一张图片上有几个歪歪扭扭的字母数字,用户需要输入出来。
2000年,卡内基梅隆大学的路易斯·冯·安(Luis von Ahn,后来还创办了多邻国)正式提出了CAPTCHA概念,并为雅虎设计了防垃圾邮件的验证码系统。
这一代的核心思路是:人类的视觉系统能轻松识别扭曲的文字,但当时的OCR技术做不到。
5.2 第二代:图像验证码(2007年开始)
随着OCR技术和机器学习的进步,文本验证码的识别率越来越高。2007年,研究人员提出了图像选择类验证码——给你一组图片,让你选出包含特定物体的图片(比如"选出所有包含红绿灯的图片")。
2015年,12306为了防黄牛抢票,推出了著名的图片点选验证码,当时因为图片质量差、识别难度高,被网友吐槽"人类都认不出来"。
这一代的核心思路是:人类能轻松理解图片语义,但机器的图像理解能力还不够。
5.3 第三代:行为验证码(2012年开始)
2012年,武汉大学论坛首次应用了滑动拼图验证码。用户需要拖动滑块,把缺失的拼图块补到正确位置。系统不仅校验最终位置,还会分析拖动过程中的鼠标轨迹——人类的拖动轨迹有加速度变化、有轻微抖动,而脚本的轨迹往往是匀速直线运动。
后来又出现了点选式验证码(“请依次点击图中的苹果、香蕉、橘子”),同样依赖行为轨迹分析。
这一代的核心思路是:不考你认不认识,而是观察你怎么操作——人类的行为模式和机器不一样。
5.4 第四代:无感验证(2018年开始)
2018年,Google推出reCAPTCHA v3,彻底取消了用户需要完成的任何操作。它在后台静默收集用户的浏览行为、设备指纹、鼠标移动轨迹等上百个特征,用机器学习模型直接给出一个风险评分。真实用户完全感知不到验证码的存在,只有高风险用户才会被要求做额外验证。
国内的极验、顶象、腾讯防水墙也都推出了类似的无感验证产品。
这一代的核心思路是:最好的验证码,是用户感觉不到它存在的验证码。
5.5 为什么我们还要学Python生成图形验证码?
看到这里你可能会问:既然都发展到第四代无感验证了,我学第一代图形验证码还有什么用?
原因很简单:
- 自建图形验证码是基础:理解了最基础的图形验证码原理,才能理解后面几代的设计思路
- 内网系统和小型项目用得上:企业内部系统、个人项目、Demo原型,没必要接入第三方付费服务
- 面试必考:图形验证码的实现是Python后端面试的高频题
- 可控性强:不依赖外部服务,数据不出内网,适合对安全合规要求高的场景
六、怎么用Python生成验证码?
Python生成验证码主要有两种方式:一种是用Pillow库从零手写,灵活度最高;另一种是用现成的captcha库,几行代码搞定。我们分别来讲。

6.1 方式一:最简版(random + string)
先从最简单的开始。如果你的需求只是生成一个随机验证码字符串(比如内推码、邀请码),不需要生成图片,那用Python内置的random和string模块就够了:
import random
import string
# 验证码字符池:大小写字母 + 数字
code_str = string.ascii_letters + string.digits
def gen_code(length=4):
"""生成指定长度的随机验证码"""
return ''.join(random.sample(code_str, length))
# 生成10个10位的验证码
print([gen_code(10) for i in range(10)])
输出结果:
['bErxkIlXOm', 'gPypzIOrnc', 'JmiUkyWopS', '3EHBmteSKw', '7J4AEyUO9R', 'eLMnjiZozE', 'NySvpYBlHE', 'FLDlW2anMH', 'yapgecnzLV', 'Q5Iz1CihtS']
这里用了random.sample(),它的特点是不会重复选取同一个字符。如果你允许重复字符,可以改用random.choices():
import random
import string
code_str = string.ascii_letters + string.digits
def gen_code_with_repeat(length=4):
"""生成允许重复字符的验证码"""
return ''.join(random.choices(code_str, k=length))
print(gen_code_with_repeat(6))
6.2 方式二:Pillow从零生成图形验证码(推荐掌握)
这是面试最常考的方式,也是灵活度最高的方式。我们需要安装Pillow库:
pip install Pillow
完整实现代码:
import random
import string
from PIL import Image, ImageDraw, ImageFont, ImageFilter
class CaptchaGenerator:
"""图形验证码生成器"""
def __init__(self, width=120, height=40, char_count=4):
self.width = width # 图片宽度
self.height = height # 图片高度
self.char_count = char_count # 验证码字符数
# 验证码字符池(去掉容易混淆的字符:0/O, 1/I/l)
self.char_pool = string.ascii_uppercase + string.digits
self.char_pool = self.char_pool.replace('O', '').replace('0', '')
self.char_pool = self.char_pool.replace('I', '').replace('1', '').replace('L', '')
def _random_color(self, min_val=0, max_val=255):
"""生成随机颜色"""
return (
random.randint(min_val, max_val),
random.randint(min_val, max_val),
random.randint(min_val, max_val)
)
def _generate_text(self):
"""生成随机验证码文本"""
return ''.join(random.choices(self.char_pool, k=self.char_count))
def generate(self):
"""生成验证码图片和文本"""
# 1. 创建画布(浅色背景)
image = Image.new('RGB', (self.width, self.height), (255, 255, 255))
draw = ImageDraw.Draw(image)
# 2. 绘制背景噪点
for _ in range(150):
x = random.randint(0, self.width)
y = random.randint(0, self.height)
draw.point((x, y), fill=self._random_color(150, 220))
# 3. 绘制干扰线
for _ in range(4):
x1 = random.randint(0, self.width)
y1 = random.randint(0, self.height)
x2 = random.randint(0, self.width)
y2 = random.randint(0, self.height)
draw.line([(x1, y1), (x2, y2)], fill=self._random_color(100, 200), width=1)
# 4. 绘制验证码文字
text = self._generate_text()
# 尝试加载系统字体,失败则使用默认字体
try:
font = ImageFont.truetype("arial.ttf", size=28)
except (IOError, OSError):
font = ImageFont.load_default()
# 每个字符随机位置、颜色、旋转
for i, char in enumerate(text):
char_x = 15 + i * (self.width - 30) // self.char_count
char_y = random.randint(2, 8)
# 创建单字符图片用于旋转
char_img = Image.new('RGBA', (30, 35), (255, 255, 255, 0))
char_draw = ImageDraw.Draw(char_img)
char_draw.text((0, 0), char, font=font, fill=self._random_color(30, 120))
# 随机旋转角度
char_img = char_img.rotate(random.randint(-20, 20), expand=True)
# 粘贴到主画布
image.paste(char_img, (char_x, char_y), char_img)
# 5. 添加模糊滤镜,增加识别难度
image = image.filter(ImageFilter.BLUR)
return image, text
# 使用示例
if __name__ == '__main__':
generator = CaptchaGenerator(width=120, height=40, char_count=4)
image, text = generator.generate()
print(f"验证码文本: {text}")
image.save('captcha.png')
print("验证码图片已保存为 captcha.png")
这段代码做了这几件事:
- 创建画布:用
Image.new()创建一张指定大小的白色背景图片 - 绘制噪点:随机画150个彩色小点,干扰OCR识别
- 绘制干扰线:画4条随机颜色的曲线,进一步增加识别难度
- 绘制文字:每个字符随机颜色、随机位置、随机旋转角度
- 模糊处理:用
ImageFilter.BLUR做轻微模糊
有几个设计细节值得注意:
- 去掉易混淆字符:把0和O、1和I和L从字符池中去掉,避免用户分不清
- 忽略大小写:实际校验时建议统一转大写或小写再比对
- 字体问题:Linux服务器上可能没有arial.ttf,可以下载一个免费字体放到项目目录里
6.3 方式三:使用captcha库(快速开发)
如果你不想自己手写绘图逻辑,可以用专门的captcha库,它还支持生成音频验证码:
pip install captcha
import random
import string
from captcha.image import ImageCaptcha
# 字符池
characters = string.digits + string.ascii_uppercase
# 创建验证码生成器,指定宽高
image_captcha = ImageCaptcha(width=160, height=60)
# 生成随机4位验证码
captcha_text = ''.join(random.choices(characters, k=4))
# 生成图片并保存
data = image_captcha.generate(captcha_text)
image_captcha.write(captcha_text, 'captcha_lib.png')
print(f"验证码: {captcha_text}")
print("图片已保存为 captcha_lib.png")
captcha库还支持音频验证码,适合无障碍场景:
from captcha.audio import AudioCaptcha
audio_captcha = AudioCaptcha()
audio_data = audio_captcha.generate('1234')
audio_captcha.write('1234', 'captcha_audio.wav')
6.4 三种方式对比
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| random+string | 零依赖,最简单 | 只能生成文本,没有图片 | 邀请码、内推码、优惠券码 |
| Pillow手写 | 完全可控,可定制性强 | 代码量稍大 | 生产环境、面试、需要定制样式 |
| captcha库 | 开箱即用,支持音频 | 样式固定,灵活度低 | 快速原型、内部工具 |
七、企业项目中如何使用?
上面讲的都是生成验证码的基础代码,但在真实企业项目中,验证码不是孤立存在的,它需要和Web框架、缓存系统、前端页面配合使用。下面我们用Flask + Redis来实现一个完整的企业级验证码方案。
7.1 项目结构
project/
├── app.py # Flask主程序
├── captcha_gen.py # 验证码生成模块
├── requirements.txt # 依赖清单
└── templates/
└── login.html # 登录页面
7.2 依赖安装
pip install Flask Pillow redis
7.3 验证码生成模块(captcha_gen.py)
import random
import string
from PIL import Image, ImageDraw, ImageFont, ImageFilter
from io import BytesIO
class CaptchaGenerator:
def __init__(self, width=120, height=40, char_count=4):
self.width = width
self.height = height
self.char_count = char_count
self.char_pool = string.ascii_uppercase + string.digits
# 移除易混淆字符
for c in ['O', '0', 'I', '1', 'L']:
self.char_pool = self.char_pool.replace(c, '')
def _random_color(self, low=0, high=255):
return (random.randint(low, high), random.randint(low, high), random.randint(low, high))
def generate_image_bytes(self):
"""生成验证码,返回图片字节和验证码文本"""
image = Image.new('RGB', (self.width, self.height), (245, 245, 245))
draw = ImageDraw.Draw(image)
# 背景噪点
for _ in range(120):
draw.point(
(random.randint(0, self.width), random.randint(0, self.height)),
fill=self._random_color(180, 230)
)
# 干扰线
for _ in range(3):
draw.line(
[(random.randint(0, self.width), random.randint(0, self.height)),
(random.randint(0, self.width), random.randint(0, self.height))],
fill=self._random_color(120, 200), width=1
)
# 生成文本
text = ''.join(random.choices(self.char_pool, k=self.char_count))
# 绘制文字
try:
font = ImageFont.truetype("DejaVuSans.ttf", 26)
except (IOError, OSError):
font = ImageFont.load_default()
for i, char in enumerate(text):
x = 10 + i * (self.width - 20) // self.char_count
y = random.randint(0, 6)
char_img = Image.new('RGBA', (28, 32), (255, 255, 255, 0))
char_draw = ImageDraw.Draw(char_img)
char_draw.text((0, 0), char, font=font, fill=self._random_color(20, 100))
char_img = char_img.rotate(random.randint(-25, 25), expand=True)
image.paste(char_img, (x, y), char_img)
# 轻微模糊
image = image.filter(ImageFilter.SMOOTH)
# 转为字节流
buf = BytesIO()
image.save(buf, format='PNG')
buf.seek(0)
return buf.getvalue(), text
7.4 Flask主程序(app.py)
import uuid
import redis
from flask import Flask, request, session, jsonify, render_template, make_response
from captcha_gen import CaptchaGenerator
app = Flask(__name__)
app.secret_key = 'your-secret-key-change-in-production'
# 连接Redis(生产环境从配置文件读取)
redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
# 验证码过期时间:5分钟
CAPTCHA_EXPIRE_SECONDS = 300
@app.route('/captcha', methods=['GET'])
def get_captcha():
"""获取验证码图片接口"""
generator = CaptchaGenerator(width=120, height=40, char_count=4)
image_bytes, text = generator.generate_image_bytes()
# 生成唯一标识,存入Redis
captcha_id = str(uuid.uuid4())
redis_client.setex(f"captcha:{captcha_id}", CAPTCHA_EXPIRE_SECONDS, text.upper())
# 返回图片,同时把captcha_id放在响应头中
response = make_response(image_bytes)
response.headers['Content-Type'] = 'image/png'
response.headers['X-Captcha-Id'] = captcha_id
return response
@app.route('/login', methods=['POST'])
def login():
"""登录接口,校验验证码"""
# 从请求头获取captcha_id
captcha_id = request.headers.get('X-Captcha-Id', '')
user_captcha = request.form.get('captcha', '').upper()
username = request.form.get('username', '')
password = request.form.get('password', '')
# 1. 校验验证码
if not captcha_id or not user_captcha:
return jsonify({'code': 400, 'msg': '请输入验证码'}), 400
redis_key = f"captcha:{captcha_id}"
stored_captcha = redis_client.get(redis_key)
if not stored_captcha:
return jsonify({'code': 400, 'msg': '验证码已过期,请刷新'}), 400
if stored_captcha != user_captcha:
# 验证失败也要删除,防止暴力尝试
redis_client.delete(redis_key)
return jsonify({'code': 400, 'msg': '验证码错误'}), 400
# 验证通过,立即删除(一次性使用)
redis_client.delete(redis_key)
# 2. 校验用户名密码(此处省略,实际项目查数据库)
# user = User.query.filter_by(username=username).first()
# if not user or not user.check_password(password):
# return jsonify({'code': 401, 'msg': '用户名或密码错误'}), 401
# 3. 登录成功
session['user_id'] = username
return jsonify({'code': 200, 'msg': '登录成功'})
@app.route('/')
def index():
return render_template('login.html')
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)
7.5 前端页面(templates/login.html)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>登录</title>
<style>
.login-box {
width: 320px;
margin: 100px auto;
padding: 30px;
border: 1px solid #ddd;
border-radius: 8px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
}
.form-group input {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
.captcha-row {
display: flex;
gap: 10px;
}
.captcha-row input {
flex: 1;
}
#captchaImg {
width: 120px;
height: 40px;
cursor: pointer;
border: 1px solid #ddd;
}
button {
width: 100%;
padding: 10px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="login-box">
<h2>用户登录</h2>
<div class="form-group">
<label>用户名</label>
<input type="text" id="username" placeholder="请输入用户名">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="password" placeholder="请输入密码">
</div>
<div class="form-group">
<label>验证码</label>
<div class="captcha-row">
<input type="text" id="captcha" placeholder="请输入验证码" maxlength="4">
<img id="captchaImg" src="/captcha" alt="验证码" title="点击刷新">
</div>
</div>
<button onclick="doLogin()">登 录</button>
</div>
<script>
let currentCaptchaId = '';
// 页面加载时获取验证码
function loadCaptcha() {
fetch('/captcha')
.then(response => {
currentCaptchaId = response.headers.get('X-Captcha-Id');
return response.blob();
})
.then(blob => {
const img = document.getElementById('captchaImg');
img.src = URL.createObjectURL(blob);
});
}
// 点击图片刷新验证码
document.getElementById('captchaImg').addEventListener('click', loadCaptcha);
loadCaptcha();
// 登录提交
function doLogin() {
const formData = new FormData();
formData.append('username', document.getElementById('username').value);
formData.append('password', document.getElementById('password').value);
formData.append('captcha', document.getElementById('captcha').value);
fetch('/login', {
method: 'POST',
headers: {
'X-Captcha-Id': currentCaptchaId
},
body: formData
})
.then(res => res.json())
.then(data => {
alert(data.msg);
if (data.code !== 200) {
loadCaptcha(); // 登录失败刷新验证码
}
});
}
</script>
</body>
</html>
7.6 企业级最佳实践
在真实项目中,还有几个关键点需要注意:
1. 限制验证失败次数
# 记录某个IP的验证码失败次数
fail_key = f"captcha_fail:{request.remote_addr}"
fail_count = redis_client.incr(fail_key)
redis_client.expire(fail_key, 300) # 5分钟内计数
if fail_count > 5:
return jsonify({'code': 429, 'msg': '验证失败次数过多,请稍后再试'}), 429
2. 接口频率限制
# 同一个IP每分钟最多请求20次验证码
rate_key = f"captcha_rate:{request.remote_addr}"
rate_count = redis_client.incr(rate_key)
if rate_count == 1:
redis_client.expire(rate_key, 60)
if rate_count > 20:
return jsonify({'code': 429, 'msg': '请求过于频繁'}), 429
3. Django项目中的集成方式
如果你用的是Django而不是Flask,思路完全一样,只是把视图函数换成Django的写法:
# views.py
import uuid
import redis
from django.http import HttpResponse, JsonResponse
from django.views.decorators.http import require_GET, require_POST
from .captcha_gen import CaptchaGenerator
redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
@require_GET
def get_captcha(request):
generator = CaptchaGenerator()
image_bytes, text = generator.generate_image_bytes()
captcha_id = str(uuid.uuid4())
redis_client.setex(f"captcha:{captcha_id}", 300, text.upper())
response = HttpResponse(image_bytes, content_type='image/png')
response['X-Captcha-Id'] = captcha_id
return response
八、验证码方案竞品对比
除了自己用Python生成验证码,市面上还有很多成熟的第三方验证码服务。我们来做一个全面的对比。

| 对比维度 | Python自建图形验证码 | Google reCAPTCHA | hCaptcha | 极验验证码 | 腾讯防水墙 |
|---|---|---|---|---|---|
| 开发成本 | 中等,需自己写代码和维护 | 低,几行JS接入 | 低,几行JS接入 | 低,SDK接入 | 低,SDK接入 |
| 安全性 | 中等,易被OCR识别 | 高,AI驱动风险评分 | 高,隐私优先的AI模型 | 高,行为轨迹分析 | 高,腾讯安全能力 |
| 用户体验 | 一般,需要手动输入 | v3无感,v2需点选 | 较好,图片选择 | 好,滑块拖动体验流畅 | 好,支持无感+滑块 |
| 隐私合规 | 最佳,数据完全不出内网 | 较差,Google收集用户行为数据 | 较好,GDPR合规 | 较好,国内合规 | 较好,国内合规 |
| 费用 | 免费,只有服务器成本 | 免费(有调用量限制) | 免费版有限,企业版付费 | 免费版有限,企业版付费 | 免费版有限,企业版付费 |
| 国内访问 | 无问题 | 不稳定,可能被墙 | 不稳定 | 稳定,国内CDN | 稳定,国内CDN |
| 可定制性 | 完全可控 | 有限 | 有限 | 中等,支持定制样式 | 中等 |
| 适用场景 | 内网系统、小型项目、对数据安全要求高 | 海外网站、Google生态项目 | 注重隐私的海外项目 | 国内中大型企业 | 国内企业,腾讯云用户 |
8.1 各方案适用场景分析
Python自建验证码适合:
- 企业内部管理系统(不对外暴露,安全要求没那么高)
- 个人项目、Demo、毕业设计
- 对数据隐私要求极高,不能把用户数据传给第三方
- 预算有限的小型项目
reCAPTCHA适合:
- 面向海外用户的网站
- 已经在使用Google生态的项目
- 个人博客、小型网站(免费够用)
极验/腾讯防水墙适合:
- 国内中大型互联网公司
- 对安全性和用户体验都有较高要求
- 有预算购买企业版服务
8.2 选型建议
- 刚起步的小项目:先用Python自建图形验证码,零成本快速上线
- 用户量起来后:考虑接入极验或腾讯防水墙,提升安全性和用户体验
- 出海项目:用reCAPTCHA或hCaptcha
- 金融、政务等高安全场景:建议行为验证码+短信验证+设备指纹的多重验证组合
九、常用场景总结

9.1 用户注册登录
这是最常见的场景。注册时加验证码防止批量注册,登录时加验证码防止暴力破解。一般策略是:密码错误3次后才要求输入验证码,避免影响正常用户体验。
9.2 短信接口保护
发送短信验证码的接口必须加图形验证码,否则你的短信费会被攻击者刷爆。这是血的教训。
9.3 表单提交防刷
留言板、评论区、反馈表单,加个验证码能过滤掉百分之九十以上的垃圾提交。
9.4 投票活动防刷
选秀投票、问卷调查、最佳评选,没有验证码的投票结果没有任何参考价值。
9.5 支付安全验证
大额支付、修改密码、绑定手机等敏感操作,建议叠加图形验证码+短信验证码的双重验证。
9.6 爬虫反爬防护
在关键数据接口前加验证码,能大幅提高爬虫的成本。当然,专业爬虫会用打码平台绕过,但至少能挡住脚本小子。
十、面试官高频面试题
10.1 问:为什么验证码要存在Redis里,存在Session里不行吗?
答: Session也可以,但有局限性。Session默认存在服务器内存中,分布式部署时多台服务器不共享Session,用户的请求落到不同服务器就会校验失败。Redis是独立的缓存服务,所有服务器都能访问,天然支持分布式。而且Redis的setex命令可以一键设置过期时间,非常适合验证码这种有有效期的场景。
10.2 问:验证码校验通过后为什么要立即删除?
答: 为了防止重放攻击。如果验证码不删除,攻击者截获一次有效的验证码后,可以反复使用它来提交请求。校验通过后立即删除,保证验证码是一次性的,这是安全设计的基本原则。
10.3 问:如何防止验证码被暴力枚举?
答: 三个手段:第一,设置过期时间,比如5分钟有效,缩短可被攻击的时间窗口;第二,限制验证失败次数,同一个IP失败5次就锁定一段时间;第三,验证码长度不要太短,4位以上,字符池足够大。
10.4 问:图形验证码容易被OCR识别,怎么提高安全性?
答: 可以从几个方面入手:增加干扰线和噪点;字符随机旋转、扭曲、重叠;使用多种颜色和字体;添加背景纹理。但要注意平衡安全性和用户体验,太复杂的验证码人类也认不出来就失去意义了。更高级的方案是升级到行为验证码或无感验证。
10.5 问:验证码比对为什么要忽略大小写?
答: 因为很多字体里大写i和小写L、数字0和字母O长得几乎一样,用户很难区分。忽略大小写能大幅降低用户输错的概率,提升体验。实现上就是把存储的验证码和用户输入都转成大写(或小写)再比对。
10.6 问:如果Redis挂了,验证码功能不可用怎么办?
答: 生产环境Redis应该做主从复制和哨兵模式,保证高可用。如果Redis确实挂了,可以降级到Session存储(单实例场景),或者临时关闭验证码(仅限内网低风险接口),同时触发告警让运维介入。关键是要有降级预案,不能因为验证码服务挂了导致整个登录功能不可用。
10.7 问:用Pillow生成验证码时,字体文件找不到怎么办?
答: 有几个方案:第一,把字体文件(比如.ttf文件)放到项目目录里,用相对路径加载;第二,用ImageFont.load_default()加载Pillow自带的默认字体(虽然比较丑但能用);第三,在Docker部署时,在Dockerfile里安装系统字体,比如apt-get install -y fonts-dejavu-core。
10.8 问:random.sample和random.choices有什么区别?
答: random.sample(population, k)是无放回抽样,选出的k个元素不会重复;random.choices(population, k=k)是有放回抽样,允许重复元素。生成验证码时,如果验证码长度大于字符池大小,就必须用choices;一般4位验证码用哪个都行,但sample能避免连续相同字符,视觉效果更好。
十一、总结
回顾一下本文的核心内容:
- 验证码是Web应用的安全底线,能挡住百分之九十九的自动化攻击
- 验证码经历了四代演进:文本验证码、图像验证码、行为验证码、无感验证
- Python生成验证码有三种方式:random生成文本、Pillow手写、captcha库
- 企业项目中验证码需要和Redis配合,做到一次性使用、设置过期、限制频率
- 选型上,小项目用自建验证码,中大型项目考虑接入第三方行为验证服务
验证码的本质是一场人和机器的军备竞赛。随着AI技术的发展,传统的图形验证码越来越容易被攻破,但它依然是成本最低、最容易实现的基础防护手段。掌握它的原理和实现,不仅能解决实际工作中的问题,也是面试中的加分项。
希望这篇文章能帮你彻底搞懂Python验证码的来龙去脉。动手跑一遍文中的代码,你会发现其实一点都不难。
转载声明:本文为原创文章,如需转载,请联系作者获得授权,并注明出处。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)