70 lines
2.2 KiB
Python
70 lines
2.2 KiB
Python
import base64
|
|
import hashlib
|
|
import hmac
|
|
import re
|
|
import secrets
|
|
from typing import Tuple
|
|
|
|
from .config import settings
|
|
|
|
|
|
_USERNAME_RE = re.compile(r"^[a-zA-Z0-9_.-]{3,64}$")
|
|
_PBKDF2_ITERATIONS = int(settings.AUTH_PBKDF2_ITERATIONS)
|
|
|
|
|
|
def normalize_username(username: str) -> str:
|
|
return (username or "").strip().lower()
|
|
|
|
|
|
def validate_username(username: str) -> Tuple[bool, str]:
|
|
normalized = normalize_username(username)
|
|
if not normalized:
|
|
return False, "用户名不能为空。"
|
|
if not _USERNAME_RE.match(normalized):
|
|
return False, "用户名仅支持 3-64 位字母、数字、下划线、点和短横线。"
|
|
return True, ""
|
|
|
|
|
|
def validate_password(password: str) -> Tuple[bool, str]:
|
|
password = password or ""
|
|
if len(password) < 8:
|
|
return False, "密码长度至少 8 位。"
|
|
if not re.search(r"[A-Z]", password):
|
|
return False, "密码需包含至少一个大写字母。"
|
|
if not re.search(r"[a-z]", password):
|
|
return False, "密码需包含至少一个小写字母。"
|
|
if not re.search(r"[0-9]", password):
|
|
return False, "密码需包含至少一个数字。"
|
|
return True, ""
|
|
|
|
|
|
def hash_password(password: str, iterations: int = _PBKDF2_ITERATIONS) -> str:
|
|
salt = secrets.token_bytes(16)
|
|
dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations)
|
|
salt_b64 = base64.b64encode(salt).decode("utf-8")
|
|
hash_b64 = base64.b64encode(dk).decode("utf-8")
|
|
return f"pbkdf2_sha256${iterations}${salt_b64}${hash_b64}"
|
|
|
|
|
|
def verify_password(password: str, encoded: str) -> bool:
|
|
try:
|
|
algo, iteration_str, salt_b64, hash_b64 = (encoded or "").split("$", 3)
|
|
if algo != "pbkdf2_sha256":
|
|
return False
|
|
iterations = int(iteration_str)
|
|
salt = base64.b64decode(salt_b64.encode("utf-8"))
|
|
expected = base64.b64decode(hash_b64.encode("utf-8"))
|
|
except Exception:
|
|
return False
|
|
|
|
actual = hashlib.pbkdf2_hmac("sha256", (password or "").encode("utf-8"), salt, iterations)
|
|
return hmac.compare_digest(actual, expected)
|
|
|
|
|
|
def generate_session_token() -> str:
|
|
return secrets.token_urlsafe(48)
|
|
|
|
|
|
def hash_session_token(token: str) -> str:
|
|
return hashlib.sha256((token or "").encode("utf-8")).hexdigest()
|