"""图形验证码的签发与校验。 校验使用 Redis `GETDEL`——**原子取出即删**,同一验证码用两次必然失败,防重放。 若写成「先 GET 再 DEL」,两条命令之间存在竞态窗口,并发请求可用同一验证码 重复提交。 这是设计代价而非缺陷:用户输错一次密码后验证码已被消费,必须换一张。 **前端必须在登录失败后自动刷新验证码**,否则第二次提交必然报「验证码错误」。 """ import uuid from app.core.captcha_image import generate_text, render_png_base64 from app.core.config import get_settings from app.core.constants import REDIS_KEY_CAPTCHA from app.core.redis_client import get_redis def issue() -> tuple[str, str, int]: """签发验证码。 :returns: ``(captcha_id, PNG 的 base64, 有效期秒数)`` 图片与 ID 在同一次调用中成对产生,避免前端分两次请求导致错配。 """ settings = get_settings() captcha_id = uuid.uuid4().hex text = generate_text() # 用 set(ex=…) 而非 setex(…):后者在 redis-py 8 已弃用(规范禁用弃用 API) get_redis().set( REDIS_KEY_CAPTCHA.format(captcha_id=captcha_id), text.upper(), ex=settings.captcha_expire_seconds, ) return captcha_id, render_png_base64(text), settings.captcha_expire_seconds def verify_and_consume(captcha_id: str, code: str) -> bool: """校验验证码并消费它。 **无论校验成败,该验证码都会失效** —— 通过 GETDEL 取出后键已被删除。 这阻断了「用一个验证码反复试密码」的路径。 :returns: 校验是否通过。验证码不存在(过期或已被使用)时返回 False。 """ expected = get_redis().getdel(REDIS_KEY_CAPTCHA.format(captcha_id=captcha_id)) if expected is None: return False return expected == code.strip().upper()