Files
loom/backend/app/core/security.py
T
2026-04-24 17:46:03 +08:00

77 lines
2.1 KiB
Python

"""密码哈希 + JWT 签发/校验。
不用 passlib(新版 bcrypt 4.x 会让 passlib 的 wrap-bug 探测崩溃),直接用 bcrypt。
对超过 72 字节的密码先做 SHA-256 + base64 预处理,规避 bcrypt 上限。
"""
from __future__ import annotations
import base64
import hashlib
from datetime import datetime, timedelta, timezone
from typing import Any
import bcrypt
from jose import JWTError, jwt
from app.core.config import settings
def _prepare(password: str) -> bytes:
raw = password.encode("utf-8")
if len(raw) > 72:
# SHA-256 固定 32 字节 → base64 后 44 字节,始终 < 72
raw = base64.b64encode(hashlib.sha256(raw).digest())
return raw
def hash_password(plain: str) -> str:
return bcrypt.hashpw(_prepare(plain), bcrypt.gensalt()).decode("ascii")
def verify_password(plain: str, hashed: str) -> bool:
try:
return bcrypt.checkpw(_prepare(plain), hashed.encode("ascii"))
except ValueError:
return False
def _now() -> datetime:
return datetime.now(timezone.utc)
def create_access_token(subject: str | int, extra: dict[str, Any] | None = None) -> str:
return _create_token(subject, settings.JWT_ACCESS_TTL, "access", extra)
def create_refresh_token(subject: str | int, extra: dict[str, Any] | None = None) -> str:
return _create_token(subject, settings.JWT_REFRESH_TTL, "refresh", extra)
def _create_token(
subject: str | int, ttl_seconds: int, token_type: str, extra: dict[str, Any] | None
) -> str:
payload: dict[str, Any] = {
"sub": str(subject),
"type": token_type,
"iat": _now(),
"exp": _now() + timedelta(seconds=ttl_seconds),
}
if extra:
payload.update(extra)
return jwt.encode(payload, settings.APP_SECRET_KEY, algorithm=settings.JWT_ALG)
def decode_token(token: str) -> dict[str, Any]:
"""解码 JWT,失败抛 `JWTError`。"""
return jwt.decode(token, settings.APP_SECRET_KEY, algorithms=[settings.JWT_ALG])
__all__ = [
"hash_password",
"verify_password",
"create_access_token",
"create_refresh_token",
"decode_token",
"JWTError",
]