checkpoint: 权限即时生效——角色/用户变更自动踢下线 + roleless 用户自读修复
- online/service: kick_user_sessions 扫描 USER_SESSION 匹配目标用户,删三键使登录时固化的权限快照即时失效 - role service+controller: 更新/删除/改状态/改权限后踢下线绑定该角色的用户 - user service+controller: 停用/改密等变更后踢下线(对应用户即时 401 重登) - permission: 无角色用户读自身记录时按 id 匹配放行(修复 409 真 bug)
This commit is contained in:
@@ -56,9 +56,10 @@ async def update_current_user_info_controller(
|
||||
async def change_current_user_password_controller(
|
||||
auth: Annotated[AuthSchema, Depends(get_current_user)],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
data: Annotated[UserChangePasswordSchema, Body(description="修改用户密码参数")],
|
||||
) -> JSONResponse:
|
||||
result_dict = await UserService(auth, db).change_password(data=data)
|
||||
result_dict = await UserService(auth, db).change_password(data=data, redis=redis)
|
||||
return SuccessResponse(data=result_dict, msg="修改密码成功, 请重新登录")
|
||||
|
||||
|
||||
@@ -66,11 +67,12 @@ async def change_current_user_password_controller(
|
||||
async def reset_password_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:update"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
id: Annotated[int, Path(description="用户ID", ge=1)],
|
||||
data: Annotated[ResetPasswordSchema, Body(description="重置用户密码参数")],
|
||||
) -> JSONResponse:
|
||||
data.id = id
|
||||
result_dict = await UserService(auth, db).reset_password(data=data)
|
||||
result_dict = await UserService(auth, db).reset_password(data=data, redis=redis)
|
||||
return SuccessResponse(data=result_dict, msg="重置密码成功")
|
||||
|
||||
|
||||
@@ -159,10 +161,11 @@ async def create_user_controller(
|
||||
async def update_user_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:update"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
id: Annotated[int, Path(description="用户ID")],
|
||||
data: Annotated[UserUpdateSchema, Body(description="修改用户参数")],
|
||||
) -> JSONResponse:
|
||||
result_dict = await UserService(auth, db).update(id=id, data=data)
|
||||
result_dict = await UserService(auth, db).update(id=id, data=data, redis=redis)
|
||||
return SuccessResponse(data=result_dict, msg="修改用户成功")
|
||||
|
||||
|
||||
@@ -170,9 +173,10 @@ async def update_user_controller(
|
||||
async def delete_user_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:delete"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
) -> JSONResponse:
|
||||
await UserService(auth, db).delete(ids=ids)
|
||||
await UserService(auth, db).delete(ids=ids, redis=redis)
|
||||
return SuccessResponse(msg="删除用户成功")
|
||||
|
||||
|
||||
@@ -180,9 +184,10 @@ async def delete_user_controller(
|
||||
async def batch_set_available_user_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_system:user:patch"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
data: Annotated[BatchSetAvailable, Body(description="状态设置")],
|
||||
) -> JSONResponse:
|
||||
await UserService(auth, db).set_available(data=data)
|
||||
await UserService(auth, db).set_available(data=data, redis=redis)
|
||||
return SuccessResponse(msg="批量修改用户状态成功")
|
||||
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@ from app.api.v1.module_system.menu.crud import MenuCRUD
|
||||
from app.api.v1.module_system.menu.schema import MenuOutSchema, MenuTreeOutSchema
|
||||
from app.api.v1.module_system.position.crud import PositionCRUD
|
||||
from app.api.v1.module_system.role.crud import RoleCRUD
|
||||
from app.common.enums import RedisInitKeyConfig
|
||||
from app.config.setting import settings
|
||||
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
|
||||
from app.core.email import send_reset_code_email
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import logger
|
||||
from app.common.enums import RedisInitKeyConfig
|
||||
from app.core.redis_crud import RedisCURD
|
||||
from app.utils.common_util import search_to_dict, traversal_to_tree
|
||||
from app.utils.excel_util import ExcelUtil
|
||||
@@ -133,7 +133,7 @@ class UserService:
|
||||
await UserCRUD(self.auth, self.db).set_user_positions(user_ids=[new_user.id], position_ids=data.position_ids)
|
||||
return await self.detail(id=new_user.id)
|
||||
|
||||
async def update(self, id: int, data: UserUpdateSchema) -> UserOutSchema:
|
||||
async def update(self, id: int, data: UserUpdateSchema, redis: Redis | None = None) -> UserOutSchema:
|
||||
if data.username:
|
||||
if exist_user := await UserCRUD(self.auth, self.db).get(username=data.username):
|
||||
if exist_user.id != id:
|
||||
@@ -174,9 +174,13 @@ class UserService:
|
||||
raise CustomException(msg="更新失败,部分岗位已被禁用")
|
||||
await UserCRUD(self.auth, self.db).set_user_positions(user_ids=[id], position_ids=data.position_ids)
|
||||
|
||||
# 角色/状态变更使登录时固化的权限快照失效,自动踢下线让其重登生效
|
||||
if redis is not None and (data.role_ids is not None or data.status is not None):
|
||||
await self._kick_users(redis, [id])
|
||||
|
||||
return await self.detail(id=id)
|
||||
|
||||
async def delete(self, ids: list[int]) -> None:
|
||||
async def delete(self, ids: list[int], redis: Redis | None = None) -> None:
|
||||
if not ids:
|
||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||
users = await UserCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)})
|
||||
@@ -199,6 +203,8 @@ class UserService:
|
||||
await UserCRUD(self.auth, self.db).set_user_roles(user_ids=ids, role_ids=[])
|
||||
await UserCRUD(self.auth, self.db).set_user_positions(user_ids=ids, position_ids=[])
|
||||
await UserCRUD(self.auth, self.db).delete(ids=ids)
|
||||
if redis is not None:
|
||||
await self._kick_users(redis, ids)
|
||||
|
||||
async def current_info(self, check_data_scope: bool = True) -> CurrentUserOutSchema:
|
||||
user_id = self.auth.user.id
|
||||
@@ -262,14 +268,16 @@ class UserService:
|
||||
await UserCRUD(self.auth, self.db).update(id=user_id, data=user_update_data)
|
||||
return await self.detail(id=user_id)
|
||||
|
||||
async def set_available(self, data: BatchSetAvailable) -> None:
|
||||
async def set_available(self, data: BatchSetAvailable, redis: Redis | None = None) -> None:
|
||||
users = await UserCRUD(self.auth, self.db).get_list(search={"id": ("in", data.ids)})
|
||||
for user in users:
|
||||
if user.is_superuser:
|
||||
raise CustomException(msg="超级管理员状态不能修改")
|
||||
await UserCRUD(self.auth, self.db).set(ids=data.ids, status=data.status)
|
||||
if redis is not None:
|
||||
await self._kick_users(redis, data.ids)
|
||||
|
||||
async def change_password(self, data: UserChangePasswordSchema) -> UserOutSchema:
|
||||
async def change_password(self, data: UserChangePasswordSchema, redis: Redis | None = None) -> UserOutSchema:
|
||||
user_id = self.auth.user.id
|
||||
if not user_id:
|
||||
raise CustomException(msg="该数据不存在")
|
||||
@@ -280,17 +288,32 @@ class UserService:
|
||||
|
||||
new_password_hash = PwdUtil.hash_password(password=data.new_password)
|
||||
await UserCRUD(self.auth, self.db).change_password(id=user_id, password_hash=new_password_hash)
|
||||
if redis is not None:
|
||||
await self._kick_users(redis, [user_id])
|
||||
return await self.detail(id=user_id)
|
||||
|
||||
async def reset_password(self, data: ResetPasswordSchema) -> UserOutSchema:
|
||||
async def reset_password(self, data: ResetPasswordSchema, redis: Redis | None = None) -> UserOutSchema:
|
||||
user = await UserCRUD(self.auth, self.db).get_or_404(id=data.id)
|
||||
if user.is_superuser:
|
||||
raise CustomException(msg="超级管理员密码不能重置")
|
||||
|
||||
new_password_hash = PwdUtil.hash_password(password=data.password)
|
||||
await UserCRUD(self.auth, self.db).change_password(id=data.id, password_hash=new_password_hash)
|
||||
if redis is not None:
|
||||
await self._kick_users(redis, [data.id])
|
||||
return await self.detail(id=data.id)
|
||||
|
||||
@staticmethod
|
||||
async def _kick_users(redis: Redis, user_ids: list[int]) -> int:
|
||||
"""踢下线指定用户的全部会话(权限/状态/密码变更后调用)。"""
|
||||
from app.api.v1.module_monitor.online.service import OnlineService
|
||||
|
||||
try:
|
||||
return await OnlineService.kick_user_sessions(redis=redis, user_ids=user_ids)
|
||||
except Exception as e:
|
||||
logger.error(f"自动踢下线失败: user_ids={user_ids}, err={e!s}")
|
||||
return 0
|
||||
|
||||
async def forget_password(self, data: UserForgetPasswordSchema) -> str:
|
||||
"""忘记密码前置校验:返回绑定邮箱(脱敏),供前端确认;不存在或未绑定邮箱则异常。"""
|
||||
user = await UserCRUD(self.auth, self.db).get_or_404(username=data.username)
|
||||
@@ -330,6 +353,7 @@ class UserService:
|
||||
new_password_hash = PwdUtil.hash_password(password=data.new_password)
|
||||
await UserCRUD(self.auth, self.db).change_password(id=user.id, password_hash=new_password_hash)
|
||||
await RedisCURD(redis).delete(redis_key)
|
||||
await self._kick_users(redis, [user.id])
|
||||
|
||||
async def register(self, data: UserRegisterSchema) -> UserOutSchema:
|
||||
"""用户注册"""
|
||||
|
||||
Reference in New Issue
Block a user