package com.iailab.module.system.service.permission; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.extra.spring.SpringUtil; import com.iailab.framework.common.enums.CommonStatusEnum; import com.iailab.framework.common.util.collection.CollectionUtils; import com.iailab.framework.datapermission.core.annotation.DataPermission; import com.iailab.module.system.api.permission.dto.DeptDataPermissionRespDTO; import com.iailab.module.system.controller.admin.permission.vo.menu.MenuListReqVO; import com.iailab.module.system.dal.dataobject.app.AppDO; import com.iailab.module.system.dal.dataobject.permission.MenuDO; import com.iailab.module.system.dal.dataobject.permission.RoleDO; import com.iailab.module.system.dal.dataobject.permission.RoleMenuDO; import com.iailab.module.system.dal.dataobject.permission.UserRoleDO; import com.iailab.module.system.dal.dataobject.tenant.TenantDO; import com.iailab.module.system.dal.dataobject.tenant.TenantPackageDO; import com.iailab.module.system.dal.mysql.permission.RoleMenuMapper; import com.iailab.module.system.dal.mysql.permission.UserRoleMapper; import com.iailab.module.system.dal.redis.RedisKeyConstants; import com.iailab.module.system.enums.permission.DataScopeEnum; import com.iailab.module.system.service.app.AppService; import com.iailab.module.system.service.dept.DeptService; import com.iailab.module.system.service.tenant.TenantPackageService; import com.iailab.module.system.service.tenant.TenantService; import com.iailab.module.system.service.user.AdminUserService; import com.baomidou.dynamic.datasource.annotation.DSTransactional; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Suppliers; import com.google.common.collect.Sets; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.*; import java.util.function.Supplier; import static com.iailab.framework.common.util.collection.CollectionUtils.convertSet; import static com.iailab.framework.common.util.json.JsonUtils.toJsonString; import static com.iailab.framework.tenant.core.context.TenantContextHolder.getTenantId; /** * 权限 Service 实现类 * * @author iailab */ @Service @Slf4j public class PermissionServiceImpl implements PermissionService { @Resource private RoleMenuMapper roleMenuMapper; @Resource private UserRoleMapper userRoleMapper; @Resource private RoleService roleService; @Resource private MenuService menuService; @Resource private DeptService deptService; @Resource private AdminUserService userService; @Resource private TenantService tenantService; @Resource private TenantPackageService tenantPackageService; @Resource private AppService appService; @Override public boolean hasAnyPermissions(Long userId, String... permissions) { // 如果为空,说明已经有权限 if (ArrayUtil.isEmpty(permissions)) { return true; } // 获得当前登录的角色。如果为空,说明没有权限 List roles = getEnableUserRoleListByUserIdFromCache(userId); if (CollUtil.isEmpty(roles)) { return false; } // 情况一:遍历判断每个权限,如果有一满足,说明有权限 for (String permission : permissions) { if (hasAnyPermission(roles, permission)) { return true; } } // 情况二:如果是超管,也说明有权限 return roleService.hasAnySuperAdmin(convertSet(roles, RoleDO::getId)); } /** * 判断指定角色,是否拥有该 permission 权限 * * @param roles 指定角色数组 * @param permission 权限标识 * @return 是否拥有 */ private boolean hasAnyPermission(List roles, String permission) { List menuIds = menuService.getMenuIdListByPermissionFromCache(permission); // 采用严格模式,如果权限找不到对应的 Menu 的话,也认为没有权限 if (CollUtil.isEmpty(menuIds)) { return false; } // 判断是否有权限 Set roleIds = convertSet(roles, RoleDO::getId); for (Long menuId : menuIds) { // 获得拥有该菜单的角色编号集合 Set menuRoleIds = getSelf().getMenuRoleIdListByMenuIdFromCache(menuId); // 如果有交集,说明有权限 if (CollUtil.containsAny(menuRoleIds, roleIds)) { return true; } } return false; } @Override public boolean hasAnyRoles(Long userId, String... roles) { // 如果为空,说明已经有权限 if (ArrayUtil.isEmpty(roles)) { return true; } // 获得当前登录的角色。如果为空,说明没有权限 List roleList = getEnableUserRoleListByUserIdFromCache(userId); if (CollUtil.isEmpty(roleList)) { return false; } // 判断是否有角色 Set userRoles = convertSet(roleList, RoleDO::getCode); return CollUtil.containsAny(userRoles, Sets.newHashSet(roles)); } // ========== 角色-菜单的相关方法 ========== @Override @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换 @CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST, allEntries = true) // allEntries 清空所有缓存,主要一次更新涉及到的 menuIds 较多,反倒批量会更快 public void assignRoleMenu(Long roleId, Set menuIds) { // 获得角色拥有菜单编号 Set dbMenuIds = convertSet(roleMenuMapper.selectListByRoleId(roleId), RoleMenuDO::getMenuId); // 计算新增和删除的菜单编号 Set menuIdList = CollUtil.emptyIfNull(menuIds); Collection createMenuIds = CollUtil.subtract(menuIdList, dbMenuIds); Collection deleteMenuIds = CollUtil.subtract(dbMenuIds, menuIdList); // 执行新增和删除。对于已经授权的菜单,不用做任何处理 if (CollUtil.isNotEmpty(createMenuIds)) { roleMenuMapper.insertBatch(CollectionUtils.convertList(createMenuIds, menuId -> { RoleMenuDO entity = new RoleMenuDO(); entity.setRoleId(roleId); entity.setMenuId(menuId); return entity; })); } if (CollUtil.isNotEmpty(deleteMenuIds)) { roleMenuMapper.deleteListByRoleIdAndMenuIds(roleId, deleteMenuIds); } } // ========== 角色-菜单的相关方法 ========== // @Override // @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换 // @CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST, // allEntries = true) // allEntries 清空所有缓存,主要一次更新涉及到的 menuIds 较多,反倒批量会更快 // public void assignRoleAppMenu(Long roleId, Set menuIds) { // // 获得角色拥有应用菜单编号 // MenuListReqVO reqVO = new MenuListReqVO(); // List appMenuList = menuService.getAppMenuList(reqVO); // Set appMenuIds = convertSet(appMenuList, MenuDO::getId); // Set dbMenuIds = convertSet(roleMenuMapper.selectListByRoleId(roleId), RoleMenuDO::getMenuId); // dbMenuIds.retainAll(appMenuIds); // // 计算新增和删除的菜单编号 // Set menuIdList = CollUtil.emptyIfNull(menuIds); // Collection createMenuIds = CollUtil.subtract(menuIdList, dbMenuIds); // Collection deleteMenuIds = CollUtil.subtract(dbMenuIds, menuIdList); // // 执行新增和删除。对于已经授权的菜单,不用做任何处理 // if (CollUtil.isNotEmpty(createMenuIds)) { // roleMenuMapper.insertBatch(CollectionUtils.convertList(createMenuIds, menuId -> { // RoleMenuDO entity = new RoleMenuDO(); // entity.setRoleId(roleId); // entity.setMenuId(menuId); // return entity; // })); // } // if (CollUtil.isNotEmpty(deleteMenuIds)) { // roleMenuMapper.deleteListByRoleIdAndMenuIds(roleId, deleteMenuIds); // } // } @Override @Transactional(rollbackFor = Exception.class) @Caching(evict = { @CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST, allEntries = true), // allEntries 清空所有缓存,此处无法方便获得 roleId 对应的 menu 缓存们 @CacheEvict(value = RedisKeyConstants.USER_ROLE_ID_LIST, allEntries = true) // allEntries 清空所有缓存,此处无法方便获得 roleId 对应的 user 缓存们 }) public void processRoleDeleted(Long roleId) { // 标记删除 UserRole userRoleMapper.deleteListByRoleId(roleId); // 标记删除 RoleMenu roleMenuMapper.deleteListByRoleId(roleId); } @Override @CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST, key = "#menuId") public void processMenuDeleted(Long menuId) { roleMenuMapper.deleteListByMenuId(menuId); } @Override public Set getRoleMenuListByRoleId(Collection roleIds) { if (CollUtil.isEmpty(roleIds)) { return Collections.emptySet(); } // 如果是管理员的情况下,获取全部菜单编号 if (roleService.hasAnySuperAdmin(roleIds)) { return convertSet(menuService.getMenuList(), MenuDO::getId); } return convertSet(roleMenuMapper.selectListByRoleId(roleIds), RoleMenuDO::getMenuId); } @Override public Set getRoleAppMenuListByRoleId(Collection roleIds) { if (CollUtil.isEmpty(roleIds)) { return Collections.emptySet(); } // 获取 tenantId Long tenantId = getTenantId(); // 如果是管理员的情况下,获取全部应用菜单编号 if (roleService.hasAnySuperAdmin(roleIds)) { MenuListReqVO reqVO = new MenuListReqVO(); return convertSet(menuService.getAppMenuList(tenantId, reqVO), MenuDO::getId); } // 如果是非管理员的情况下,获得拥有的应用菜单编号 TenantDO tenant = tenantService.getTenant(tenantId); TenantPackageDO tenantPackage = tenantPackageService.getTenantPackage(tenant.getPackageId()); Set menuIds = tenantPackage.getMenuIds(); Set longs = convertSet(roleMenuMapper.selectListByRoleId(roleIds), RoleMenuDO::getMenuId); longs.retainAll(menuIds); return longs; } @Override @Cacheable(value = RedisKeyConstants.MENU_ROLE_ID_LIST, key = "#menuId") public Set getMenuRoleIdListByMenuIdFromCache(Long menuId) { return convertSet(roleMenuMapper.selectListByMenuId(menuId), RoleMenuDO::getRoleId); } // ========== 用户-角色的相关方法 ========== @Override @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换 @CacheEvict(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId") public void assignUserRole(Long userId, Set roleIds) { // 获得角色拥有角色编号 Set dbRoleIds = convertSet(userRoleMapper.selectListByUserId(userId), UserRoleDO::getRoleId); // 计算新增和删除的角色编号 Set roleIdList = CollUtil.emptyIfNull(roleIds); Collection createRoleIds = CollUtil.subtract(roleIdList, dbRoleIds); Collection deleteMenuIds = CollUtil.subtract(dbRoleIds, roleIdList); // 执行新增和删除。对于已经授权的角色,不用做任何处理 if (!CollectionUtil.isEmpty(createRoleIds)) { userRoleMapper.insertBatch(CollectionUtils.convertList(createRoleIds, roleId -> { UserRoleDO entity = new UserRoleDO(); entity.setUserId(userId); entity.setRoleId(roleId); return entity; })); } if (!CollectionUtil.isEmpty(deleteMenuIds)) { userRoleMapper.deleteListByUserIdAndRoleIdIds(userId, deleteMenuIds); } } @Override @CacheEvict(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId") public void processUserDeleted(Long userId) { userRoleMapper.deleteListByUserId(userId); } @Override public Set getUserRoleIdListByUserId(Long userId) { return convertSet(userRoleMapper.selectListByUserId(userId), UserRoleDO::getRoleId); } @Override @Cacheable(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId") public Set getUserRoleIdListByUserIdFromCache(Long userId) { return getUserRoleIdListByUserId(userId); } @Override public Set getUserRoleIdListByRoleId(Collection roleIds) { return convertSet(userRoleMapper.selectListByRoleIds(roleIds), UserRoleDO::getUserId); } /** * 获得用户拥有的角色,并且这些角色是开启状态的 * * @param userId 用户编号 * @return 用户拥有的角色 */ @VisibleForTesting List getEnableUserRoleListByUserIdFromCache(Long userId) { // 获得用户拥有的角色编号 Set roleIds = getSelf().getUserRoleIdListByUserIdFromCache(userId); // 获得角色数组,并移除被禁用的 List roles = roleService.getRoleListFromCache(roleIds); roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); return roles; } // ========== 用户-部门的相关方法 ========== @Override public void assignRoleDataScope(Long roleId, Integer dataScope, Set dataScopeDeptIds) { roleService.updateRoleDataScope(roleId, dataScope, dataScopeDeptIds); } @Override @DataPermission(enable = false) // 关闭数据权限,不然就会出现递归获取数据权限的问题 public DeptDataPermissionRespDTO getDeptDataPermission(Long userId) { // 获得用户的角色 List roles = getEnableUserRoleListByUserIdFromCache(userId); // 如果角色为空,则只能查看自己 DeptDataPermissionRespDTO result = new DeptDataPermissionRespDTO(); if (CollUtil.isEmpty(roles)) { result.setSelf(true); return result; } // 获得用户的部门编号的缓存,通过 Guava 的 Suppliers 惰性求值,即有且仅有第一次发起 DB 的查询 Supplier userDeptId = Suppliers.memoize(() -> userService.getUser(userId).getDeptId()); // 遍历每个角色,计算 for (RoleDO role : roles) { // 为空时,跳过 if (role.getDataScope() == null) { continue; } // 情况一,ALL if (Objects.equals(role.getDataScope(), DataScopeEnum.ALL.getScope())) { result.setAll(true); continue; } // 情况二,DEPT_CUSTOM if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_CUSTOM.getScope())) { CollUtil.addAll(result.getDeptIds(), role.getDataScopeDeptIds()); // 自定义可见部门时,保证可以看到自己所在的部门。否则,一些场景下可能会有问题。 // 例如说,登录时,基于 t_user 的 username 查询会可能被 dept_id 过滤掉 CollUtil.addAll(result.getDeptIds(), userDeptId.get()); continue; } // 情况三,DEPT_ONLY if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_ONLY.getScope())) { CollectionUtils.addIfNotNull(result.getDeptIds(), userDeptId.get()); continue; } // 情况四,DEPT_DEPT_AND_CHILD if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_AND_CHILD.getScope())) { CollUtil.addAll(result.getDeptIds(), deptService.getChildDeptIdListFromCache(userDeptId.get())); // 添加本身部门编号 CollUtil.addAll(result.getDeptIds(), userDeptId.get()); continue; } // 情况五,SELF if (Objects.equals(role.getDataScope(), DataScopeEnum.SELF.getScope())) { result.setSelf(true); continue; } // 未知情况,error log 即可 log.error("[getDeptDataPermission][LoginUser({}) role({}) 无法处理]", userId, toJsonString(result)); } return result; } /** * 获得自身的代理对象,解决 AOP 生效问题 * * @return 自己 */ private PermissionServiceImpl getSelf() { return SpringUtil.getBean(getClass()); } }