mirror of
https://github.com/stardrophere/InsightRadar.git
synced 2026-06-06 00:57:51 +08:00
彻底删除数据库记录验证码
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user