You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
buffer_platform/buffer_platform_v2/app/auth_service.py

126 lines
3.8 KiB

"""认证服务模块 - 密码哈希、令牌管理、会话管理、默认管理员创建"""
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")