nex_math/backend/services/security.py

57 lines
1.6 KiB
Python

"""密码散列与 JWT 工具。"""
from __future__ import annotations
import hashlib
import hmac
import os
import secrets
from datetime import datetime, timedelta, timezone
import jwt
PBKDF2_ITERATIONS = 200_000
def hash_password(password: str) -> str:
salt = secrets.token_hex(16)
digest = hashlib.pbkdf2_hmac(
"sha256", password.encode("utf-8"), bytes.fromhex(salt), PBKDF2_ITERATIONS
).hex()
return f"pbkdf2_sha256${PBKDF2_ITERATIONS}${salt}${digest}"
def verify_password(password: str, stored: str) -> bool:
try:
algorithm, iterations, salt, expected = stored.split("$")
if algorithm != "pbkdf2_sha256":
return False
digest = hashlib.pbkdf2_hmac(
"sha256", password.encode("utf-8"), bytes.fromhex(salt), int(iterations)
).hex()
return hmac.compare_digest(digest, expected)
except (ValueError, TypeError):
return False
def _jwt_secret() -> str:
return os.getenv("JWT_SECRET", "nex-math-dev-secret-change-me-in-production")
def create_access_token(user_id: int, expires_hours: int = 24 * 7) -> str:
now = datetime.now(timezone.utc)
payload = {
"sub": str(user_id),
"iat": now,
"exp": now + timedelta(hours=expires_hours),
}
return jwt.encode(payload, _jwt_secret(), algorithm="HS256")
def decode_access_token(token: str) -> int | None:
try:
payload = jwt.decode(token, _jwt_secret(), algorithms=["HS256"])
return int(payload["sub"])
except (jwt.PyJWTError, KeyError, ValueError):
return None