彻底删除数据库记录验证码

This commit is contained in:
2026-03-26 01:48:55 +08:00
parent 2335b62384
commit b18901a2d5
12 changed files with 444 additions and 417 deletions
@@ -0,0 +1,71 @@
# app/verification/backends/memory.py
from functools import lru_cache
import time
import json
import threading
from typing import Optional
from app.models.models import VerificationPurpose
from app.core.verification.email.verificationRepository import VerificationRepository
class MemoryRepository(VerificationRepository):
def __init__(self):
self._store = {}
self._lock = threading.Lock()
def _key(self, email: str, purpose: VerificationPurpose) -> str:
email = email.lower()
return f"verification:code:{purpose.value.lower()}:{email}"
def set_code(self, email: str, purpose: VerificationPurpose, code_hash: str, ttl: int) -> None:
key = self._key(email, purpose)
expire_at = time.time() + ttl
payload = {
"code_hash": code_hash
}
with self._lock:
self._store[key] = (json.dumps(payload), expire_at)
def consume_code(self, email: str, purpose: VerificationPurpose) -> Optional[str]:
key = self._key(email, purpose)
with self._lock:
data = self._store.get(key)
if not data:
return None
value, expire_at = data
if time.time() > expire_at:
del self._store[key]
return None
del self._store[key]
try:
payload = json.loads(value)
return payload.get("code_hash")
except Exception:
return None
def incr(self, key: str, ttl: int) -> int:
now = time.time()
with self._lock:
value, expire = self._store.get(key, (0, 0))
if now > expire:
value = 0
value += 1
self._store[key] = (value, now + ttl)
return value
@lru_cache
def get_memory_repo():
return MemoryRepository()
@@ -0,0 +1,88 @@
from functools import lru_cache
import os
import logging
import json
import datetime
import redis
from typing import Optional, TYPE_CHECKING
from app.models.models import VerificationPurpose
from app.core.verification.email.verificationRepository import VerificationRepository
from app.utils.redis_client import get_redis_client
from app.core.security import hash_verification_code
logger = logging.getLogger(__name__)
AUTH_CODE_REDIS_PREFIX = os.getenv("AUTH_CODE_REDIS_PREFIX", "insightradar:auth_code").strip()
class RedisRepository(VerificationRepository):
_consume_lua = """ local val = redis.call("GET", KEYS[1]) if val then redis.call("DEL", KEYS[1]) end return val """
def __init__(self, client: redis.Redis):
self.client = client
self._consume_script = self.client.register_script(self._consume_lua)
def _key(self, email, purpose):
return f"{AUTH_CODE_REDIS_PREFIX}:{purpose.value.lower()}:{email}:code"
def set_code(self, email: str, purpose: VerificationPurpose, code_hash: str, ttl: int) -> None:
"""store the code into the redis
Args:
email (str): email of user
purpose (VerificationPurpose): purpose of the code, such as "login", "register"
code_hash: the hash of the code
ttl: duration of the code
"""
key = self._key(email, purpose)
payload = json.dumps({
"code_hash": code_hash,
"exp": datetime.datetime.now().timestamp()
})
self.client.set(key, payload, ex=ttl)
def consume_code(self, email: str, purpose: VerificationPurpose) -> Optional[str]:
"""consume the code of email
Args:
email (str): email of user
purpose (VerificationPurpose): purpose of the code, such as "login", "register"
Returns:
_type_: if email has a code which has not been consumed, return the code, else return None
"""
key = self._key(email, purpose)
data = self._consume_script(keys=[key])
if not data:
return None
try:
payload = json.loads(data) # type: ignore
return payload.get("code_hash")
except Exception:
return None
def incr(self, key: str, ttl: int) -> int:
super().incr(key, ttl)
value = self.client.incr(key)
if value == 1:
self.client.expire(key, ttl)
return value # type: ignore
@lru_cache
def get_redis_repo():
client = get_redis_client()
if client is None:
return None
return RedisRepository(client)
@@ -0,0 +1,54 @@
# app/verification/backends/hybrid.py
from functools import lru_cache
import logging
from typing import Optional
from app.models.models import VerificationPurpose
from app.core.verification.email.verificationRepository import VerificationRepository
from app.core.verification.email.RespositoryImpl.MemoryRepository import get_memory_repo
from app.core.verification.email.RespositoryImpl.RedisRepository import get_redis_repo
logger = logging.getLogger(__name__)
class HybridRepository(VerificationRepository):
def __init__(self, redis_repo: Optional[VerificationRepository], memory_repo: VerificationRepository):
self.redis = redis_repo
self.memory = memory_repo
def set_code(self, email: str, purpose: VerificationPurpose, code_hash: str, ttl: int) -> None:
if self.redis:
try:
self.redis.set_code(email, purpose, code_hash, ttl)
return
except Exception as e:
logger.warning("Redis set_code failed, fallback to memory: %s", e)
self.memory.set_code(email, purpose, code_hash, ttl)
def consume_code(self, email: str, purpose: VerificationPurpose) -> Optional[str]:
if self.redis:
try:
return self.redis.consume_code(email, purpose)
except Exception as e:
logger.warning("Redis consume_code failed, fallback to memory: %s", e)
return self.memory.consume_code(email, purpose)
def incr(self, key: str, ttl: int) -> int:
if self.redis:
try:
return self.redis.incr(key, ttl)
except Exception as e:
logger.warning("Redis incr failed, fallback to memory: %s", e)
return self.memory.incr(key, ttl)
@lru_cache
def get_verification_repository():
redis_repo = get_redis_repo()
memory_repo = get_memory_repo()
return HybridRepository(redis_repo, memory_repo)
@@ -0,0 +1,44 @@
from abc import abstractmethod, ABC
from typing import Optional
from app.models.models import VerificationPurpose
class VerificationRepository(ABC):
"""验证码持久层抽象基类"""
@abstractmethod
def set_code(self, email: str, purpose: VerificationPurpose, code_hash: str, ttl: int) -> None:
"""write the code into the storage
Args:
email (str): email of user
purpose (VerificationPurpose): the purpose of the code, such as, "login", "register"
code_hash (str): hash of the code
ttl (int): duration
"""
pass
@abstractmethod
def consume_code(self, email: str, purpose: VerificationPurpose) -> Optional[str]:
"""consume the code atomically
Args:
email (str): email of user
purpose (VerificationPurpose): the purpose of the code, such as, "login", "register"
Returns:
Optional[str]: if success return the code, else return None
"""
pass
@abstractmethod
def incr(self, key: str, ttl: int) -> int:
"""create a counter in the storage, it will be delete after ttl
Args:
key (str): key of the counter
ttl (int): duration
"""
pass
@@ -0,0 +1,79 @@
from functools import lru_cache
import os
import logging
from typing import Optional
from app.models.models import VerificationPurpose
from app.core.verification.email.verificationRepository import VerificationRepository
from app.core.security import generate_verification_code, hash_verification_code
from app.core.verification.email.RespositoryImpl.hybirdRepository import get_verification_repository
logger = logging.getLogger(__name__)
# 注册验证码有效期(分钟)
REGISTER_CODE_EXPIRE_MINUTES = os.getenv("REGISTER_CODE_EXPIRE_MINUTES", 5)
# 登录验证码有效期(分钟)
LOGIN_CODE_EXPIRE_MINUTES = os.getenv("LOGIN_CODE_EXPIRE_MINUTES",5)
# 同一邮箱发送验证码的冷却间隔(秒)
CODE_SEND_COOLDOWN_SECONDS = os.getenv("CODE_SEND_COOLDOWN_SECONDS",60)
class CodeExpiredError(Exception):
"""code has been expired"""
pass
class CodeInvalidError(Exception):
"""code is not right"""
pass
class TooManyCodeRequestsError(Exception):
"""User request too many times when they verificate the email"""
pass
def get_ttl(purpose: VerificationPurpose)->int:
if purpose == VerificationPurpose.LOGIN:
return int(LOGIN_CODE_EXPIRE_MINUTES) * 60
else:
return int(REGISTER_CODE_EXPIRE_MINUTES) * 60
class EmailVerificationService:
def __init__(self, repo: VerificationRepository) -> None:
self.repo = repo
def _cooldown_key(self, email: str, purpose: VerificationPurpose) -> str:
return f"verification:cooldown:{purpose.value}:{email.lower()}"
def send_code(self, email: str, purpose: VerificationPurpose) -> str:
email = email.lower()
count = self.repo.incr(self._cooldown_key(email, purpose), int(CODE_SEND_COOLDOWN_SECONDS))
if count > 1:
raise TooManyCodeRequestsError("Please wait before requesting another code")
code = generate_verification_code()
code_hash = hash_verification_code(code)
self.repo.set_code(email, purpose, code_hash, get_ttl(purpose))
return code
def verify_code(self,email: str, code: str, purpose: VerificationPurpose):
email = email.lower()
stored_hash: Optional[str] = self.repo.consume_code(email, purpose)
if not stored_hash:
raise CodeExpiredError("Code expired or not found")
if stored_hash != hash_verification_code(code):
raise CodeInvalidError("Invalid code")
return True
@lru_cache
def get_verification_service():
repo = get_verification_repository()
return EmailVerificationService(repo)