chore: 基线提交(重构前代码快照)

- git 初始化 + .gitignore 补全(根 .env / logs / instance / __pycache__ 等)
- 修复 utils/.gitignore 误将整个 utils/ 源码层忽略的问题,补入全部工具源码(含 ip2region.xdb)
- 含后端全部代码:application / extensions / utils / config / migrations
- 含前端 ui/(后续整体迁走)
- 不含根 .env(含密钥,本地保留)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
DjangoAdmin Dev
2026-08-07 12:12:06 +08:00
co-authored by Claude
commit 15f2a5e097
591 changed files with 95894 additions and 0 deletions
+232
View File
@@ -0,0 +1,232 @@
# +======================================================================
# | 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