2025-05-02 23:56:59 +08:00
|
|
|
import os
|
2026-05-04 09:33:47 +08:00
|
|
|
import secrets
|
2025-10-13 15:08:54 +08:00
|
|
|
from datetime import timedelta
|
2025-05-23 15:30:14 +08:00
|
|
|
from typing import Any
|
2025-05-02 23:56:59 +08:00
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
import jwt
|
2026-04-02 16:03:01 +08:00
|
|
|
from argon2 import PasswordHasher
|
2026-05-17 13:06:25 +08:00
|
|
|
from argon2.exceptions import InvalidHash, VerifyMismatchError, VerificationError
|
2026-03-17 10:16:44 +08:00
|
|
|
from yuxi.utils.datetime_utils import utc_now
|
2025-10-13 15:08:54 +08:00
|
|
|
|
2025-05-02 23:56:59 +08:00
|
|
|
# JWT配置
|
2026-05-04 09:33:47 +08:00
|
|
|
LEGACY_JWT_SECRET_KEY = "yuxi_know_secure_key"
|
2025-05-02 23:56:59 +08:00
|
|
|
JWT_ALGORITHM = "HS256"
|
2025-09-07 20:06:56 +08:00
|
|
|
JWT_EXPIRATION = 7 * 24 * 60 * 60 # 7天过期
|
2026-05-04 09:33:47 +08:00
|
|
|
JWT_AUDIENCE = "yuxi-know-api"
|
2026-04-02 16:03:01 +08:00
|
|
|
PASSWORD_HASHER = PasswordHasher()
|
2025-05-02 23:56:59 +08:00
|
|
|
|
2025-09-01 22:37:03 +08:00
|
|
|
|
2026-05-04 09:33:47 +08:00
|
|
|
def _is_production_env() -> bool:
|
|
|
|
|
return os.environ.get("YUXI_ENV", "development").strip().lower() in {"prod", "production"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_or_create_dev_env(name: str, value_factory) -> str:
|
|
|
|
|
value = os.environ.get(name, "").strip()
|
|
|
|
|
if value:
|
|
|
|
|
return value
|
|
|
|
|
if _is_production_env():
|
|
|
|
|
raise ValueError(f"{name} 未配置,请在生产环境的 .env.prod 中设置持久化随机值")
|
|
|
|
|
|
|
|
|
|
value = value_factory()
|
|
|
|
|
os.environ[name] = value
|
|
|
|
|
print(f"{name} 未配置,开发环境已自动生成临时随机值,服务重启后会重新生成。")
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_jwt_secret_key() -> str:
|
|
|
|
|
secret_key = _get_or_create_dev_env("JWT_SECRET_KEY", lambda: secrets.token_hex(32))
|
|
|
|
|
if secret_key == LEGACY_JWT_SECRET_KEY:
|
|
|
|
|
raise ValueError("JWT_SECRET_KEY 不能使用历史默认密钥,请重新生成随机强密钥")
|
|
|
|
|
return secret_key
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_jwt_issuer() -> str:
|
|
|
|
|
instance_id = _get_or_create_dev_env("YUXI_INSTANCE_ID", lambda: f"instance-{secrets.token_hex(8)}")
|
|
|
|
|
return f"yuxi-know:{instance_id}"
|
|
|
|
|
|
|
|
|
|
|
2025-05-02 23:56:59 +08:00
|
|
|
class AuthUtils:
|
|
|
|
|
"""认证工具类"""
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def hash_password(password: str) -> str:
|
2026-04-02 16:03:01 +08:00
|
|
|
"""使用 Argon2 哈希密码"""
|
|
|
|
|
return PASSWORD_HASHER.hash(password)
|
2025-05-02 23:56:59 +08:00
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def verify_password(stored_password: str, provided_password: str) -> bool:
|
|
|
|
|
"""验证密码"""
|
2026-05-17 13:06:25 +08:00
|
|
|
if not stored_password.startswith("$argon2"):
|
|
|
|
|
return False
|
|
|
|
|
try:
|
|
|
|
|
return PASSWORD_HASHER.verify(stored_password, provided_password)
|
|
|
|
|
except (InvalidHash, VerifyMismatchError, VerificationError):
|
2025-05-02 23:56:59 +08:00
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
2025-05-23 15:30:14 +08:00
|
|
|
def create_access_token(data: dict[str, Any], expires_delta: timedelta | None = None) -> str:
|
2025-05-02 23:56:59 +08:00
|
|
|
"""创建JWT访问令牌"""
|
|
|
|
|
to_encode = data.copy()
|
|
|
|
|
|
|
|
|
|
# 设置过期时间
|
|
|
|
|
if expires_delta:
|
2025-10-13 15:08:54 +08:00
|
|
|
expire = utc_now() + expires_delta
|
2025-05-02 23:56:59 +08:00
|
|
|
else:
|
2025-10-13 15:08:54 +08:00
|
|
|
expire = utc_now() + timedelta(seconds=JWT_EXPIRATION)
|
2025-05-02 23:56:59 +08:00
|
|
|
|
2026-05-04 09:33:47 +08:00
|
|
|
to_encode.update({"exp": expire, "iss": _get_jwt_issuer(), "aud": JWT_AUDIENCE})
|
2025-05-02 23:56:59 +08:00
|
|
|
|
|
|
|
|
# 编码JWT
|
2026-05-04 09:33:47 +08:00
|
|
|
encoded_jwt = jwt.encode(to_encode, _get_jwt_secret_key(), algorithm=JWT_ALGORITHM)
|
2025-05-02 23:56:59 +08:00
|
|
|
return encoded_jwt
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
2025-05-23 15:30:14 +08:00
|
|
|
def decode_token(token: str) -> dict[str, Any] | None:
|
2025-05-02 23:56:59 +08:00
|
|
|
"""解码验证JWT令牌"""
|
|
|
|
|
try:
|
2026-05-04 09:33:47 +08:00
|
|
|
payload = jwt.decode(
|
|
|
|
|
token,
|
|
|
|
|
_get_jwt_secret_key(),
|
|
|
|
|
algorithms=[JWT_ALGORITHM],
|
|
|
|
|
issuer=_get_jwt_issuer(),
|
|
|
|
|
audience=JWT_AUDIENCE,
|
|
|
|
|
options={"require": ["exp", "sub", "iss", "aud"]},
|
|
|
|
|
)
|
2025-05-02 23:56:59 +08:00
|
|
|
return payload
|
2026-05-04 09:33:47 +08:00
|
|
|
except (jwt.PyJWTError, ValueError):
|
2025-05-02 23:56:59 +08:00
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
2025-05-23 15:30:14 +08:00
|
|
|
def verify_access_token(token: str) -> dict[str, Any]:
|
2025-05-02 23:56:59 +08:00
|
|
|
"""验证访问令牌,如果无效则抛出异常"""
|
|
|
|
|
try:
|
2026-05-04 09:33:47 +08:00
|
|
|
payload = jwt.decode(
|
|
|
|
|
token,
|
|
|
|
|
_get_jwt_secret_key(),
|
|
|
|
|
algorithms=[JWT_ALGORITHM],
|
|
|
|
|
issuer=_get_jwt_issuer(),
|
|
|
|
|
audience=JWT_AUDIENCE,
|
|
|
|
|
options={"require": ["exp", "sub", "iss", "aud"]},
|
|
|
|
|
)
|
2025-05-02 23:56:59 +08:00
|
|
|
return payload
|
|
|
|
|
except jwt.ExpiredSignatureError:
|
|
|
|
|
raise ValueError("令牌已过期")
|
|
|
|
|
except jwt.InvalidTokenError:
|
2025-05-23 15:30:14 +08:00
|
|
|
raise ValueError("无效的令牌")
|