20、Python 教程 - 使用python生成验证码

使用Python生成验证码封面

文章目录

一、痛点场景:没有验证码的网站到底有多惨?

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

这就是没有验证码的真实代价。

验证码痛点场景图

在实际开发中,我们经常遇到这些让人头疼的问题:

  • 注册接口被刷爆:恶意脚本一秒钟注册上百个账号,用来发垃圾广告、薅羊毛
  • 短信接口被轰炸:攻击者利用你的短信验证码接口,给任意手机号发垃圾短信,你的短信账单瞬间爆炸
  • 登录接口被暴力破解:黑客用字典不断尝试密码,没有验证码的话几小时就能破解弱密码
  • 投票活动被操纵:选秀投票、最佳评选,脚本一秒投一千票,真实用户的声音被淹没
  • 评论区被垃圾信息淹没:自动发帖机器人到处发广告链接,你的社区变成垃圾场
  • 爬虫疯狂抓取数据:你的核心数据被竞争对手一键爬走,带宽成本飙升

我曾经接手过一个项目,上线初期没有加验证码,上线第三天短信接口就被人盯上了,一晚上发了三万条短信,损失了将近两万块钱的短信费。加上之后,攻击量直接下降了百分之九十九。

所以,验证码不是锦上添花的功能,而是Web应用的安全底线。

二、痛点的解决方案:验证码系统的完整架构

那么验证码到底是怎么解决这些问题的呢?我们来看一个完整的验证码系统架构:

验证码解决方案架构图

整个流程分为六步:

  1. 用户请求验证码:浏览器向服务器发起获取验证码的请求
  2. 服务器生成验证码:服务器生成随机字符串,用图像处理库绘制成图片
  3. 缓存验证码:把验证码文本存入Redis或Session,设置过期时间(通常5分钟)
  4. 返回图片给用户:把生成的图片返回给前端展示
  5. 用户提交验证码:用户输入看到的字符,连同表单一起提交
  6. 服务器校验:从缓存中取出验证码,和用户输入比对,校验通过后立即删除(防止重复使用)

这里有几个关键的安全细节:

  • 验证码必须一次性使用,校验通过后立刻删除,不能重复校验
  • 必须设置过期时间,防止验证码被暴力枚举
  • 验证码比对时建议忽略大小写,提升用户体验
  • 生产环境建议用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生成图形验证码?

看到这里你可能会问:既然都发展到第四代无感验证了,我学第一代图形验证码还有什么用?

原因很简单:

  1. 自建图形验证码是基础:理解了最基础的图形验证码原理,才能理解后面几代的设计思路
  2. 内网系统和小型项目用得上:企业内部系统、个人项目、Demo原型,没必要接入第三方付费服务
  3. 面试必考:图形验证码的实现是Python后端面试的高频题
  4. 可控性强:不依赖外部服务,数据不出内网,适合对安全合规要求高的场景

六、怎么用Python生成验证码?

Python生成验证码主要有两种方式:一种是用Pillow库从零手写,灵活度最高;另一种是用现成的captcha库,几行代码搞定。我们分别来讲。

Python生成验证码流程图

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")

这段代码做了这几件事:

  1. 创建画布:用Image.new()创建一张指定大小的白色背景图片
  2. 绘制噪点:随机画150个彩色小点,干扰OCR识别
  3. 绘制干扰线:画4条随机颜色的曲线,进一步增加识别难度
  4. 绘制文字:每个字符随机颜色、随机位置、随机旋转角度
  5. 模糊处理:用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 reCAPTCHAhCaptcha极验验证码腾讯防水墙
开发成本中等,需自己写代码和维护低,几行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能避免连续相同字符,视觉效果更好。

十一、总结

回顾一下本文的核心内容:

  1. 验证码是Web应用的安全底线,能挡住百分之九十九的自动化攻击
  2. 验证码经历了四代演进:文本验证码、图像验证码、行为验证码、无感验证
  3. Python生成验证码有三种方式:random生成文本、Pillow手写、captcha库
  4. 企业项目中验证码需要和Redis配合,做到一次性使用、设置过期、限制频率
  5. 选型上,小项目用自建验证码,中大型项目考虑接入第三方行为验证服务

验证码的本质是一场人和机器的军备竞赛。随着AI技术的发展,传统的图形验证码越来越容易被攻破,但它依然是成本最低、最容易实现的基础防护手段。掌握它的原理和实现,不仅能解决实际工作中的问题,也是面试中的加分项。

希望这篇文章能帮你彻底搞懂Python验证码的来龙去脉。动手跑一遍文中的代码,你会发现其实一点都不难。


转载声明:本文为原创文章,如需转载,请联系作者获得授权,并注明出处。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐