fix(oidc): 占位用户标记为is_deleted=1,禁止参与实际登录,符合注释说明

This commit is contained in:
Your Name 2026-04-16 14:53:42 +08:00
parent ea530039c9
commit 5db5b847b4

View File

@ -470,19 +470,30 @@ async def find_user_by_oidc_sub(db, sub: str) -> User | None:
"""通过 OIDC sub 查找用户"""
# 方法1: 检查是否有用户的 user_id 直接等于 "oidc:{sub}"(标准 OIDC 用户)
standard_oidc_user_id = f"oidc:{sub}"
result = await db.execute(select(User).filter(User.user_id == standard_oidc_user_id, User.is_deleted == 0))
# 占位绑定记录会被标记为 is_deleted=1但我们仍需要查询它们来获取绑定关系
result = await db.execute(select(User).filter(
User.user_id == standard_oidc_user_id,
User.is_deleted == 0
))
user = result.scalar_one_or_none()
if user:
return user
# 方法2: 检查是否有绑定占位用户格式: "oidc:{sub}:{target_user_id}"use_raw_username 绑定记录)
# 绑定占位用户被标记为 is_deleted=1需要包括deleted来查询
legacy_result = await db.execute(
select(User).filter(User.user_id.like(f"{standard_oidc_user_id}:%"), User.is_deleted == 0).order_by(User.id.asc())
select(User).filter(
User.user_id.like(f"{standard_oidc_user_id}:%"),
User.is_deleted.in_([0, 1])
).order_by(User.id.asc())
)
legacy_users = list(legacy_result.scalars().all())
if legacy_users:
# 对于绑定占位用户user_id 格式为 oidc:{sub}:{target_user_id},解析出 target_user_id 并返回真实用户
for placeholder in legacy_users:
if placeholder.is_deleted != 1:
# 非deleted占位直接返回
return placeholder
parts = placeholder.user_id.split(":")
if len(parts) >= 3:
try:
@ -494,7 +505,10 @@ async def find_user_by_oidc_sub(db, sub: str) -> User | None:
return target_user
except ValueError:
continue
# 如果没有解析出有效的目标用户返回第一个legacy用户向后兼容
# 如果没有解析出有效的目标用户返回第一个非deleted legacy用户向后兼容
for candidate in legacy_users:
if candidate.is_deleted == 0:
return candidate
if len(legacy_users) > 1:
logger.warning(f"Multiple legacy OIDC users matched for sub={sub}, use earliest id={legacy_users[0].id}")
return legacy_users[0]
@ -511,9 +525,12 @@ async def find_deleted_oidc_user_by_sub(db, sub: str) -> User | None:
if deleted_user:
return deleted_user
# 检查绑定占位格式 oidc:{sub}:{target_user_id}
# 检查绑定占位格式 oidc:{sub}:{target_user_id}占位本身是deleted需要查询目标用户
legacy_result = await db.execute(
select(User).filter(User.user_id.like(f"{oidc_user_id}:%"), User.is_deleted == 1).order_by(User.id.asc())
select(User).filter(
User.user_id.like(f"{oidc_user_id}:%"),
User.is_deleted == 1
).order_by(User.id.asc())
)
legacy_users = list(legacy_result.scalars().all())
if legacy_users:
@ -535,8 +552,9 @@ async def find_deleted_oidc_user_by_sub(db, sub: str) -> User | None:
async def _create_oidc_binding_placeholder(db, sub: str, target_user: User) -> None:
"""创建 OIDC sub 绑定占位用户(仅用于记录绑定关系,不用于登录)
use_raw_username 模式下我们创建一个占位用户格式: oidc:{sub}:{target_user_id}
以便通过 find_user_by_oidc_sub 解析出绑定的真实用户并返回
use_raw_username 模式下我们创建一个占位用户格式: oidc:{sub}:{target_user_id},
占位用户标记为 is_deleted=1不参与实际登录仅用于存储绑定关系
find_user_by_oidc_sub 查询时会读取该占位记录并解析出绑定的真实用户
这样就能在不修改User表结构的前提下保持绑定关系可验证防止账号冒用
"""
from yuxi.repositories.user_repository import UserRepository
@ -544,12 +562,13 @@ async def _create_oidc_binding_placeholder(db, sub: str, target_user: User) -> N
# 占位用户格式: oidc:{sub}:{target_user_id}这样find_user_by_oidc_sub可以解析出目标用户ID
oidc_placeholder_id = f"oidc:{sub}:{target_user.id}"
result = await db.execute(select(User).filter(User.user_id == oidc_placeholder_id, User.is_deleted == 0))
# 占位用户标记为 deleted查询时需要特别包括deleted才能找到
result = await db.execute(select(User).filter(User.user_id == oidc_placeholder_id, User.is_deleted.in_([0, 1])))
if result.scalar_one_or_none():
# 占位用户已存在,无需重复创建
return
# 创建占位用户:使用随机密码,不用于实际登录,仅存储绑定关系
# 创建占位用户:使用随机密码,标记为deleted不用于实际登录,仅存储绑定关系
random_password = secrets.token_urlsafe(32)
password_hash = AuthUtils.hash_password(random_password)
@ -562,9 +581,10 @@ async def _create_oidc_binding_placeholder(db, sub: str, target_user: User) -> N
"password_hash": password_hash,
"role": target_user.role,
"department_id": target_user.department_id,
"is_deleted": 1, # 标记为deleted不参与实际登录
"last_login": utc_now_naive(),
})
logger.info(f"Created OIDC binding placeholder for sub {sub} -> user {target_user.id} ({target_user.user_id})")
logger.info(f"Created OIDC binding placeholder (deleted) for sub {sub} -> user {target_user.id} ({target_user.user_id})")
except IntegrityError:
# 并发创建冲突,忽略
await db.rollback()