Become a sponsor

验证码
验证码是防止自动化攻击的重要安全手段。使用 Pillow 生成图片验证码,存储在 Redis 中,支持防刷限流和万能验证码(演示模式)。源码位于 src/utils/captcha.py。
1. 前端请求 GET /api/v1/captcha
2. 后端生成随机字符 → 绘制图片(干扰线+噪点)→ Base64 编码
3. 字符存入 Redis(key=UUID,TTL=300秒)
4. 返回 {key, captcha} 给前端
5. 前端展示图片,用户输入验证码
6. 登录时后端从 Redis 取出比对,验证成功后自动删除(防重放)| 环境变量 | 默认值 | 说明 |
|---|---|---|
CAPTCHA_LENGTH | 6 | 验证码字符长度 |
CAPTCHA_EXPIRE | 300 | 过期时间(秒) |
CAPTCHA_RATE_WINDOW | 60 | 防刷窗口期(秒) |
CAPTCHA_RATE_LIMIT | 10 | 窗口期内最大获取次数 |
CAPTCHA_CHARS | ABCDEFGHJKLMNPQRSTUVWXYZ2346789 | 字符集(去除易混淆字符) |
CAPTCHA_FONT_PATH | static/fonts/Vera.ttf | 字体路径 |
CAPTCHA_IMAGE_WIDTH | 160 | 图片宽度 |
CAPTCHA_IMAGE_HEIGHT | 50 | 图片高度 |
CAPTCHA_FONT_SIZE | 28 | 字体大小 |
CAPTCHA_NOISE_LINES | 6 | 干扰线数量 |
CAPTCHA_NOISE_DOTS | 30 | 噪点数量 |
CAPTCHA_BYPASS_CODE | 618618 | 万能验证码(仅演示模式生效) |
GET /api/v1/captcha响应示例:
{
"code": 0,
"msg": "操作成功",
"data": {
"key": "54e66743-0617-46fd-9b7e-cc1044ac1ccd",
"captcha": "data:image/png;base64,iVBORw0KGgo..."
},
"ok": true
}验证码校验在登录流程中自动完成,核心逻辑在 src/utils/captcha.py:
from utils.captcha import verify_captcha
# 验证成功后自动删除,防止重放攻击
is_valid = await verify_captcha(key, code)验证码校验采用 Lua 脚本实现原子消费,防止并发重放和暴力破解:
# Lua 脚本:Redis 单线程原子执行 get -> del -> compare
_CAPTCHA_VERIFY_LUA = """
local stored = redis.call('get', KEYS[1])
if not stored then return 0 end
-- 消费验证码:一次性使用,失败也立即失效(防暴力破解/无限试错)
redis.call('del', KEYS[1])
local function norm(s)
return string.upper(tostring(s)):gsub('^%s+', ''):gsub('%s+$', '')
end
if norm(stored) == norm(ARGV[1]) then return 1 end
return 0
"""
async def verify_captcha(id_key: str, user_input: str) -> bool:
"""验证验证码(原子消费:验证码无论对错一次性使用)"""
if not id_key or not user_input:
return False
redis = get_redis()
# 演示模式万能验证码
if FASTAPI_DEBUG and CAPTCHA_BYPASS_CODE and user_input == CAPTCHA_BYPASS_CODE:
return True
return bool(await redis.eval(_CAPTCHA_VERIFY_LUA, 1, id_key, user_input))原子消费
验证码无论对错都会被删除(Lua 脚本内 get + del + compare 原子执行),同一 idKey 无法再次尝试。这既防止了重放攻击,也防止了暴力穷举。
验证码获取接口内置限流保护,使用 incr_with_expire Lua 脚本实现原子计数:
async def generate_captcha(request):
redis = get_redis()
# 防刷:按 IP 维度限流
client_ip = get_client_ip(request)
rate_key = f"captcha:rate:{client_ip}"
count = await redis.get(rate_key)
if count and int(count) >= CAPTCHA_RATE_LIMIT:
ttl = await redis.ttl(rate_key)
remaining = ttl if ttl and ttl > 0 else CAPTCHA_RATE_WINDOW
raise BusinessException(msg=f"请求过于频繁,请等待 {remaining} 秒后再试")
# 生成验证码 ...
image, code = _generate_image(CAPTCHA_LENGTH)
# 存储到 Redis
id_key = secrets.token_urlsafe(24)
await redis.set(id_key, code, ex=CAPTCHA_EXPIRE)
# 原子递增并设置过期(Lua 脚本,防止 incr 与 expire 之间崩溃导致 key 永久残留)
from core.redis_client import incr_with_expire
await incr_with_expire(redis, rate_key, CAPTCHA_RATE_WINDOW)限流参数:
BusinessException 提示剩余等待秒数在演示模式(FASTAPI_DEMO=True)下,万能验证码 618618 可绕过正常验证码校验,方便演示环境登录。
安全提示
生产环境务必确保 FASTAPI_DEMO=False,否则万能验证码将失效。
// 获取验证码
async function getCaptcha() {
const res = await axios.get('/api/v1/captcha')
captchaKey.value = res.data.data.key
captchaImg.value = res.data.data.captcha
}
// 登录时提交
const loginData = {
username: 'admin',
password: '123456',
code: userInput.value,
key: captchaKey.value
}的验证码方案具备以下特点:
1. 图片验证码:Pillow 生成,支持干扰线、噪点、自定义字体
2. Redis 存储:TTL 自动过期,Lua 原子消费防重放
3. 防刷限流:IP 维度 + incr_with_expire 原子计数,防止恶意刷取
4. 万能验证码:演示模式下可用,生产环境自动禁用
5. 字符集优化:去除易混淆字符(0/O、1/I/L 等)
6. 安全随机数:使用 secrets 模块生成验证码和唯一 ID