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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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