Files

68 lines
2.0 KiB
Python

import base64
import io
import secrets
from PIL import Image, ImageDraw, ImageFont
from app.services.redis_client import get_redis
CAPTCHA_TTL_SEC = 120
CAPTCHA_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
def _key(captcha_id: str) -> str:
return f"gsfdc:captcha:{captcha_id}"
def _new_code() -> str:
return "".join(secrets.choice(CAPTCHA_ALPHABET) for _ in range(4))
def _image(code: str) -> str:
width, height = 132, 48
image = Image.new("RGB", (width, height), (246, 248, 251))
draw = ImageDraw.Draw(image)
font = ImageFont.load_default(size=26)
for _ in range(5):
draw.line(
(
secrets.randbelow(width),
secrets.randbelow(height),
secrets.randbelow(width),
secrets.randbelow(height),
),
fill=(180, 190, 200),
width=1,
)
for index, char in enumerate(code):
color = (secrets.randbelow(80), secrets.randbelow(80), 40 + secrets.randbelow(80))
draw.text((12 + index * 30, 8 + secrets.randbelow(8)), char, font=font, fill=color)
buffer = io.BytesIO()
image.save(buffer, format="PNG")
encoded = base64.b64encode(buffer.getvalue()).decode("ascii")
return f"data:image/png;base64,{encoded}"
def issue_captcha() -> dict:
captcha_id = secrets.token_hex(16)
code = _new_code()
get_redis().set(_key(captcha_id), code.lower(), ex=CAPTCHA_TTL_SEC)
return {"captchaId": captcha_id, "image": _image(code)}
def consume_captcha(captcha_id: str, code: str) -> bool:
if not isinstance(captcha_id, str) or not isinstance(code, str):
return False
captcha_id = captcha_id.strip()
code = code.strip()
if not captcha_id or not code:
return False
stored = get_redis().getdel(_key(captcha_id))
if stored is None:
return False
if isinstance(stored, bytes):
stored = stored.decode("utf-8", errors="replace")
if not isinstance(stored, str):
return False
return stored.lower() == code.lower()