Skip to content

验证码

验证码是防止自动化攻击的重要安全手段。使用 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_LENGTH6验证码字符长度
CAPTCHA_EXPIRE300过期时间(秒)
CAPTCHA_RATE_WINDOW60防刷窗口期(秒)
CAPTCHA_RATE_LIMIT10窗口期内最大获取次数
CAPTCHA_CHARSABCDEFGHJKLMNPQRSTUVWXYZ2346789字符集(去除易混淆字符)
CAPTCHA_FONT_PATHstatic/fonts/Vera.ttf字体路径
CAPTCHA_IMAGE_WIDTH160图片宽度
CAPTCHA_IMAGE_HEIGHT50图片高度
CAPTCHA_FONT_SIZE28字体大小
CAPTCHA_NOISE_LINES6干扰线数量
CAPTCHA_NOISE_DOTS30噪点数量
CAPTCHA_BYPASS_CODE618618万能验证码(仅演示模式生效)

接口说明

获取验证码

GET /api/v1/captcha

响应示例:

json
{
    "code": 0,
    "msg": "操作成功",
    "data": {
        "key": "54e66743-0617-46fd-9b7e-cc1044ac1ccd",
        "captcha": "data:image/png;base64,iVBORw0KGgo..."
    },
    "ok": true
}

验证码校验

验证码校验在登录流程中自动完成,核心逻辑在 src/utils/captcha.py

python
from utils.captcha import verify_captcha

# 验证成功后自动删除,防止重放攻击
is_valid = await verify_captcha(key, code)

验证码校验采用 Lua 脚本实现原子消费,防止并发重放和暴力破解:

python
# 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 脚本实现原子计数:

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

限流参数:

  • 按 IP 维度限流
  • 窗口期内(默认60秒)最多获取10次
  • 超限后返回 BusinessException 提示剩余等待秒数
  • 防止恶意刷取验证码消耗服务器资源

万能验证码

在演示模式(FASTAPI_DEMO=True)下,万能验证码 618618 可绕过正常验证码校验,方便演示环境登录。

安全提示

生产环境务必确保 FASTAPI_DEMO=False,否则万能验证码将失效。

前端集成

javascript
// 获取验证码
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

小蚂蚁云团队 · 提供技术支持