|
|
"""认证服务模块 - 密码哈希、令牌管理、会话管理、默认管理员创建"""
|
|
|
|
|
|
import hashlib
|
|
|
import logging
|
|
|
import secrets
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
from sqlalchemy import select
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
from app.config import settings
|
|
|
from app.models.user import Session, User
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
def hash_password(password: str) -> str:
|
|
|
"""使用 SHA256 + 随机盐 对密码进行哈希。"""
|
|
|
salt = secrets.token_hex(16)
|
|
|
hashed = hashlib.sha256(f"{salt}{password}".encode()).hexdigest()
|
|
|
return f"{salt}:{hashed}"
|
|
|
|
|
|
|
|
|
def verify_password(password: str, password_hash: str) -> bool:
|
|
|
"""验证密码是否与哈希值匹配。
|
|
|
|
|
|
兼容两种分隔符格式:
|
|
|
- 新版: salt:hash(冒号分隔)
|
|
|
- 旧版: salt$hash(美元符号分隔,从原版迁移的数据)
|
|
|
"""
|
|
|
# 尝试冒号分隔(新版)
|
|
|
if ":" in password_hash:
|
|
|
salt, expected = password_hash.split(":", 1)
|
|
|
elif "$" in password_hash:
|
|
|
# 美元符号分隔(旧版迁移数据)
|
|
|
salt, expected = password_hash.split("$", 1)
|
|
|
else:
|
|
|
return False
|
|
|
actual = hashlib.sha256(f"{salt}{password}".encode()).hexdigest()
|
|
|
return secrets.compare_digest(actual, expected)
|
|
|
|
|
|
|
|
|
async def create_token(user_id: int, db: AsyncSession) -> str:
|
|
|
"""为用户创建会话令牌并持久化。
|
|
|
|
|
|
Returns:
|
|
|
生成的令牌字符串。
|
|
|
"""
|
|
|
token = secrets.token_urlsafe(32)
|
|
|
session = Session(
|
|
|
user_id=user_id,
|
|
|
token=token,
|
|
|
expires_at=datetime.utcnow() + timedelta(hours=settings.jwt_expire_hours),
|
|
|
is_valid=True,
|
|
|
)
|
|
|
db.add(session)
|
|
|
await db.commit()
|
|
|
return token
|
|
|
|
|
|
|
|
|
async def verify_token(token: str, db: AsyncSession) -> User | None:
|
|
|
"""验证令牌有效性,返回对应的 User 对象或 None。"""
|
|
|
stmt = select(Session).where(
|
|
|
Session.token == token,
|
|
|
Session.is_valid == True, # noqa: E712
|
|
|
Session.expires_at > datetime.utcnow(),
|
|
|
)
|
|
|
result = await db.execute(stmt)
|
|
|
session = result.scalar_one_or_none()
|
|
|
if not session:
|
|
|
return None
|
|
|
|
|
|
stmt_user = select(User).where(User.id == session.user_id)
|
|
|
result_user = await db.execute(stmt_user)
|
|
|
user = result_user.scalar_one_or_none()
|
|
|
if not user or not user.is_active:
|
|
|
return None
|
|
|
|
|
|
return user
|
|
|
|
|
|
|
|
|
async def authenticate_user(
|
|
|
db: AsyncSession, username: str, password: str
|
|
|
) -> User | None:
|
|
|
"""验证用户名和密码,返回 User 对象或 None。"""
|
|
|
stmt = select(User).where(User.username == username)
|
|
|
result = await db.execute(stmt)
|
|
|
user = result.scalar_one_or_none()
|
|
|
if not user or not verify_password(password, user.password_hash):
|
|
|
return None
|
|
|
return user
|
|
|
|
|
|
|
|
|
async def invalidate_session(token: str, db: AsyncSession) -> None:
|
|
|
"""使指定令牌失效。"""
|
|
|
stmt = select(Session).where(Session.token == token)
|
|
|
result = await db.execute(stmt)
|
|
|
session = result.scalar_one_or_none()
|
|
|
if session:
|
|
|
session.is_valid = False
|
|
|
await db.flush()
|
|
|
|
|
|
|
|
|
async def update_last_login(user: User, db: AsyncSession) -> None:
|
|
|
"""更新用户最后登录时间。"""
|
|
|
user.last_login = datetime.utcnow()
|
|
|
await db.flush()
|
|
|
|
|
|
|
|
|
async def create_default_admin(db: AsyncSession) -> None:
|
|
|
"""如果数据库中不存在管理员账户 lxy_root,则创建默认管理员。"""
|
|
|
stmt = select(User).where(User.username == "lxy_root")
|
|
|
result = await db.execute(stmt)
|
|
|
admin = result.scalar_one_or_none()
|
|
|
if not admin:
|
|
|
user = User(
|
|
|
username="lxy_root",
|
|
|
password_hash=hash_password("admin123"),
|
|
|
email="admin@system.local",
|
|
|
role="admin",
|
|
|
is_active=True,
|
|
|
)
|
|
|
db.add(user)
|
|
|
await db.commit()
|
|
|
logger.info("默认管理员账户已创建: lxy_root / admin123")
|