68 lines
2.1 KiB
Python
68 lines
2.1 KiB
Python
import base64
|
|
import random
|
|
import time
|
|
|
|
from app.services.auth import create_access_token
|
|
from app.utils.redis import get_redis
|
|
|
|
# In-memory fallback when Redis is not available
|
|
_captcha_store: dict[str, tuple[int, float]] = {}
|
|
|
|
|
|
async def generate_slider_captcha() -> dict:
|
|
"""Generate a slider captcha challenge."""
|
|
captcha_id = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=16))
|
|
|
|
redis = get_redis()
|
|
if redis:
|
|
await redis.setex(f"captcha:{captcha_id}", 300, "1")
|
|
else:
|
|
_captcha_store[captcha_id] = (True, time.time() + 300)
|
|
|
|
bg_data = _create_placeholder_image(300, 150, "#e2e8f0", "拖动滑块到最右侧完成验证")
|
|
slider_data = _create_placeholder_image(50, 50, "#6366f1", "")
|
|
|
|
return {
|
|
"captcha_id": captcha_id,
|
|
"background_image": bg_data,
|
|
"slider_image": slider_data,
|
|
"slider_width": 50,
|
|
}
|
|
|
|
|
|
async def verify_slider_captcha(captcha_id: str, x_offset: int) -> str | None:
|
|
"""Verify slider captcha. Return a captcha_token on success."""
|
|
redis = get_redis()
|
|
valid = False
|
|
|
|
if redis:
|
|
stored = await redis.get(f"captcha:{captcha_id}")
|
|
if stored:
|
|
valid = True
|
|
await redis.delete(f"captcha:{captcha_id}")
|
|
else:
|
|
entry = _captcha_store.pop(captcha_id, None)
|
|
if entry:
|
|
_, expires = entry
|
|
if time.time() < expires:
|
|
valid = True
|
|
|
|
if not valid:
|
|
return None
|
|
|
|
# User must drag at least 70% of the way (x_offset >= 182 out of 260)
|
|
if x_offset >= 180:
|
|
return create_access_token(f"captcha:{captcha_id}")
|
|
return None
|
|
|
|
|
|
def _create_placeholder_image(width: int, height: int, color: str, text: str) -> str:
|
|
svg = (
|
|
f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}">'
|
|
f'<rect width="100%" height="100%" fill="{color}" rx="8"/>'
|
|
f'<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" '
|
|
f'font-family="sans-serif" font-size="14" fill="#64748b">{text}</text>'
|
|
f'</svg>'
|
|
)
|
|
return f"data:image/svg+xml;base64,{base64.b64encode(svg.encode()).decode()}"
|