68 lines
2.0 KiB
Python
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()
|