# +====================================================================== # | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] # +---------------------------------------------------------------------- # | Copyright (c) 2021-2026 一米阳光团队 版权所有 # +---------------------------------------------------------------------- # | 开源协议: Apache-2.0 # | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 # +---------------------------------------------------------------------- # | 官方网站: https://www.djangoadmin.cn # +---------------------------------------------------------------------- # | 研发团队: 一米阳光团队 荣誉出品 # +====================================================================== """ 验证码生成与验证模块 ============================================================= 功能说明: 1. 生成带干扰的验证码图片,返回 Base64 格式数据 2. 验证用户输入的验证码是否正确 3. 防刷机制:限制同一 IP 获取验证码的频率 技术特点: - 使用 secrets 模块生成安全随机数,替代 random 模块 - 字符集去除 0/O/1/I/l/5/S/2/Z 等易混淆字符,提升用户体验 - 验证码图片包含干扰线和噪点,防止 OCR 识别 - 验证码存储于 Redis,支持过期自动失效 - 验证成功后自动删除,确保一次性使用,防止重放攻击 - 大小写不敏感比较,提升用户体验 配置来源: 所有配置参数从 config 模块导入,由 .env 文件控制 包括:验证码长度、过期时间、防刷间隔、字符集、字体、图片尺寸等 依赖: - utils.redis_client: Redis 客户端,用于存储验证码 - config: 验证码配置参数(从 .env 读取) - PIL: 图片生成与处理 接口说明: generate_captcha() -> dict 生成验证码,返回 {"idKey": "唯一标识", "data": "base64图片数据"} 调用频率受 CAPTCHA_RATE_LIMIT 限制 verify_captcha(id_key, user_input) -> bool 验证用户输入的验证码是否正确 验证成功后自动删除 Redis 中的记录 """ import secrets from io import BytesIO from PIL import Image, ImageFont, ImageDraw, ImageFilter import base64 from utils.redis_client import redis from config import ( CAPTCHA_LENGTH, CAPTCHA_EXPIRE, CAPTCHA_RATE_LIMIT, CAPTCHA_RATE_WINDOW, CAPTCHA_CHARS, CAPTCHA_FONT_PATH, CAPTCHA_IMAGE_WIDTH, CAPTCHA_IMAGE_HEIGHT, CAPTCHA_FONT_SIZE, CAPTCHA_NOISE_LINES, CAPTCHA_NOISE_DOTS, ) # ============================================================ # 内部函数 # ============================================================ def get_random_color(): """获取随机颜色RGB(使用secrets更安全)""" return ( secrets.randbelow(256), secrets.randbelow(256), secrets.randbelow(256) ) def generate_image(length): """生成验证码图片 Args: length: 验证码长度 Returns: tuple: (PIL.Image对象, 验证码字符串) """ # 从字符集随机抽取 code = ''.join(secrets.choice(CAPTCHA_CHARS) for _ in range(length)) # 创建画布(浅灰色背景,减少对比度干扰) image = Image.new('RGB', (CAPTCHA_IMAGE_WIDTH, CAPTCHA_IMAGE_HEIGHT), color=(240, 240, 240)) # 加载字体 try: font = ImageFont.truetype(CAPTCHA_FONT_PATH, size=CAPTCHA_FONT_SIZE) except Exception: font = ImageFont.load_default() draw = ImageDraw.Draw(image) # 绘制验证码字符(随机偏移,防OCR识别) char_width = (CAPTCHA_IMAGE_WIDTH - 20) // length for i, c in enumerate(code): x = 10 + i * char_width + secrets.randbelow(8) y = secrets.randbelow(10) + 2 draw.text( (x, y), text=c, fill=get_random_color(), font=font ) # 绘制干扰线 for _ in range(CAPTCHA_NOISE_LINES): x1 = secrets.randbelow(CAPTCHA_IMAGE_WIDTH) y1 = secrets.randbelow(CAPTCHA_IMAGE_HEIGHT // 2) x2 = secrets.randbelow(CAPTCHA_IMAGE_WIDTH) y2 = secrets.randbelow(CAPTCHA_IMAGE_HEIGHT // 2) + CAPTCHA_IMAGE_HEIGHT // 2 draw.line(((x1, y1), (x2, y2)), fill=get_random_color(), width=1) # 添加噪点 for _ in range(CAPTCHA_NOISE_DOTS): x = secrets.randbelow(CAPTCHA_IMAGE_WIDTH) y = secrets.randbelow(CAPTCHA_IMAGE_HEIGHT) draw.point((x, y), fill=get_random_color()) # 滤镜增强边缘 image = image.filter(ImageFilter.EDGE_ENHANCE) return image, code def get_client_ip(): """获取客户端真实IP(支持代理)""" from flask import request if request.headers.get('X-Forwarded-For'): return request.headers.get('X-Forwarded-For').split(',')[0].strip() return request.remote_addr # ============================================================ # 对外接口 # ============================================================ def generate_captcha(): """生成验证码实例 Returns: dict: { "idKey": str, # 验证码唯一标识 "data": str # base64图片数据 } """ from flask import request # ============================================================ # 防刷:滑动窗口计数,限制同一IP在窗口期内的请求次数 # ============================================================ client_ip = get_client_ip() rate_key = f"captcha:rate:{client_ip}" count = redis.get(rate_key) if count and int(count) >= CAPTCHA_RATE_LIMIT: ttl = redis.ttl(rate_key) remaining = ttl if ttl > 0 else CAPTCHA_RATE_WINDOW raise Exception(f"请求过于频繁,请等待 {remaining} 秒后再试") # ============================================================ # 生成验证码 # ============================================================ image, code = generate_image(CAPTCHA_LENGTH) # 转换为base64 buffer = BytesIO() image.save(buffer, format='PNG') buf_bytes = buffer.getvalue() data = base64.b64encode(buf_bytes).decode('ascii') # 生成唯一ID(使用secrets更安全) id_key = secrets.token_urlsafe(24) # 存储验证码到Redis redis.set(id_key, code, expire=CAPTCHA_EXPIRE) # 递增计数器,首次时设置过期时间(滑动窗口) if count: redis.incr(rate_key) else: redis.set(rate_key, 1, expire=CAPTCHA_RATE_WINDOW) return { "idKey": id_key, "data": data } def verify_captcha(id_key, user_input): """验证验证码(验证成功后自动删除,一次性使用) Args: id_key: 验证码ID user_input: 用户输入的验证码 Returns: bool: 验证是否通过 """ if not id_key or not user_input: return False # 从Redis获取验证码 stored_code = redis.get(id_key) if not stored_code: return False # 转为大写比较(大小写不敏感) stored_code = str(stored_code).upper().strip() user_input = str(user_input).upper().strip() # 验证通过后删除(一次性使用,防止重放攻击) if stored_code == user_input: redis.delete(id_key) return True return False