PayrollManagementSystem/backend/app/services/captcha_service.py
liuwenxin 273ebf4657 chore: 初始化项目结构与登录鉴权模块
后端 FastAPI 六层结构(api/services/repositories/models/schemas/core),
统一响应信封、全局异常、日志脱敏;6 张表经 Alembic 迁移建库,SQL 留存
db/migration;登录鉴权含图形验证码(Redis GETDEL 防重放)、JWT、登录限流、
登出黑名单(TTL=令牌剩余有效期)、登录日志。

前端 Vue3 + TS + Element Plus 骨架,含登录页、路由守卫、请求封装与 Vite 代理。

工程配置:uv 锁定 Python 3.12、ruff 检查与格式化、editorconfig、gitattributes。

测试 184 项(单元 155 / 集成 29)全绿,浏览器端到端 8 项通过。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-09 18:46:36 +08:00

49 lines
1.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""图形验证码的签发与校验。
校验使用 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()