From 32d03e3eb46573d0da6a31848356fea58ad0ee97 Mon Sep 17 00:00:00 2001 From: chenxing Date: Tue, 11 Aug 2026 21:38:14 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A8=A1=E5=9D=97=E6=9D=83=E9=99=90=E4=BF=AE?= =?UTF-8?q?=E8=AE=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/GroupAuthzService.java | 185 +++++++++++------- 1 file changed, 114 insertions(+), 71 deletions(-) diff --git a/src/main/java/com/yunhai/userbase/service/impl/GroupAuthzService.java b/src/main/java/com/yunhai/userbase/service/impl/GroupAuthzService.java index de7645e..9dddf0c 100644 --- a/src/main/java/com/yunhai/userbase/service/impl/GroupAuthzService.java +++ b/src/main/java/com/yunhai/userbase/service/impl/GroupAuthzService.java @@ -604,18 +604,26 @@ public class GroupAuthzService implements IGroupAuthzService { // 角色授权不依赖组归属与组开通应用,故下方不再因无组归属而提前返回 Set roleModuleIds = collectEnabledRoleModuleIds(userId); - // 1. 用户所属的全部组 + 【强制包含接口传入的 groupId 上下文】 - // 场景修复:管理员在「组8的授权面板」给模块M打勾(写 sys_group_module_perm 组8的记录), - // 然后同面板点「用户权限一览」UserPermissionTree?userId=X&groupId=8,必须读到组8的设置, - // 即便用户X还没加入组8(预览模式、或界面只做"组视角下的权限查询");否则"本组设置"不生效。 + // 1. 归属组集合:区分「组上下文模式」和「用户全局模式」 + // A) 接口显式传了 groupId(= 管理员在"某个组的授权面板"点用户权限一览,是典型的组视角查询): + // 以传入的 groupId 作为唯一 selfGroupId,仅沿「该组 -> 祖先 -> 第一个学校组(含)」链路判定; + // 【关键】不再把用户真实加入的其他组(例如用户还属于学校组)并入判断。 + // 原因:如果允许用户归属的"学校组"作为独立 selfGroupId 参与本组显式判定, + // 则子组未设置的模块会直接命中学校组自己的显式记录(permMap.get(学校组)!=null), + // 导致"子组设置的覆盖限制"完全失效=父级所有模块都拿到,即你观察到的"本组继承没生效/还是父级全量"。 + // B) 未传 groupId(全局查用户权限/开放接口 modules 未传schId以外的其他上下文): + // 正常取用户所有真实归属组并集。 List userRels = sysUserGroupRelRepository.findByUserId(userId); Set selfGroupIds = new LinkedHashSet<>(); if (groupId != null) { + // 组上下文模式:只认传入的 groupId,不认其他真实归属组(避免越级命中显式记录) selfGroupIds.add(groupId); - } - for (SysUserGroupRel rel : userRels) { - if (rel.getGroupId() != null) { - selfGroupIds.add(rel.getGroupId()); + } else { + // 用户全局模式:按真实归属组并集 + for (SysUserGroupRel rel : userRels) { + if (rel.getGroupId() != null) { + selfGroupIds.add(rel.getGroupId()); + } } } @@ -667,9 +675,11 @@ public class GroupAuthzService implements IGroupAuthzService { } Set allModuleIds = new LinkedHashSet<>(); Map> appModulesMap = new HashMap<>(); + Map moduleToAppMap = new HashMap<>(); for (AppModule m : allModules) { allModuleIds.add(m.getId()); appModulesMap.computeIfAbsent(m.getAppId(), k -> new ArrayList<>()).add(m); + moduleToAppMap.put(m.getId(), m.getAppId()); } // 6. 批量查询所有相关模块在所有相关组/祖先上的权限记录 @@ -686,13 +696,26 @@ public class GroupAuthzService implements IGroupAuthzService { } } + // AI-FIX(应用级覆盖): 某组只要对该应用有任意显式模块配置,子孙/本组就只按该组配置判定, + // 不再继承更上层祖先的同应用模块权限(避免「子组只勾了1个模块,却仍继承学校全部模块」)。 + Map> groupConfiguredApps = new HashMap<>(); + for (Map.Entry> entry : modulePermMap.entrySet()) { + Integer appId = moduleToAppMap.get(entry.getKey()); + if (appId == null) { + continue; + } + for (Integer gid : entry.getValue().keySet()) { + groupConfiguredApps.computeIfAbsent(gid, k -> new HashSet<>()).add(appId); + } + } + // 7. 判定每个模块是否有权限 // AI-FIX(角色授权): 最终模块权限 = 组授权模块 ∪ 角色授权模块 Set permittedModuleIds = new LinkedHashSet<>(); for (AppModule m : allModules) { if (roleModuleIds.contains(m.getId()) || hasUserModulePermission(m.getId(), m.getAppId(), selfGroupIds, chainMap, groupToApps, - modulePermMap, allGroupMap)) { + modulePermMap, allGroupMap, groupConfiguredApps)) { permittedModuleIds.add(m.getId()); } } @@ -859,29 +882,57 @@ public class GroupAuthzService implements IGroupAuthzService { } /** - * 用户维度权限判定:用户对指定模块是否有权限(两段式 + 学校截止 + 超管兜底(超管在入口短路,这里只处理普通用户))。 + * 就近原则:在 groupId 的祖先链上,找到离它最近的、对目标应用有显式模块配置的组(含自身)。 * - *

- * 两段式(用户拍板:边界1选B): - * ① 先查「本组」(归属组 selfGroupId)有没有 sys_group_module_perm - * 记录(仅记录存在即=显式设置,无论inherit_flag值)→ 有 = 本组说了算,直接true,不再查祖先; - * ② 本组无记录 = 没有显式设置 = 进入继承查找:沿祖先链向上找,直到第一个学校组(包含学校组本身,不跨学校到教育局/根),第一个「记录存在且 - * inherit_flag=true」的组即继承成功; - * ③ 学校组之上的节点(区域/教育局/根)都不查找,避免跨学校继承到未开通的应用(用户新增边界3)。 + *

链首即自身({@link #ancestorChain} 由自身向根排列),天然「就近优先」;遍历时第一个 + * 「groupConfiguredApps 中包含该应用」的组即作为该应用在本组范围内的唯一权限来源返回。 * - *

- * 其余不变:归属组或其祖先需已开通该模块所属应用(双保险,防越权)。 + *

学校截止:遇到学校组(无论是否已配置)即作为继承上限,不再向教育局/根越级,避免跨学校继承到未开通的应用。 + * + *

找不到返回 null(该应用在此范围内没有任何组的显式配置,对应模块不许可)。 + */ + private Integer findNearestAuthorityGroup(Integer groupId, List chain, + Integer appId, Map> groupConfiguredApps, + Map allGroupMap) { + if (chain == null || chain.isEmpty()) { + return null; + } + for (Integer gid : chain) { + Set apps = groupConfiguredApps.get(gid); + if (apps != null && apps.contains(appId)) { + return gid; + } + SysGroup g = allGroupMap.get(gid); + if (g != null && g.getType() == GroupTypeEnum.SCHOOL_GROUP) { + break; + } + } + return null; + } + + /** + * 用户维度权限判定:用户对指定模块是否有权限(就近原则 + 学校截止 + 角色授权并集 + 超管兜底)。 + * + *

就近原则(统一了「本组覆盖」与「最近祖先继承」两种情形,修复两类合并 bug): + * 沿 selfGroupId 的祖先链(含自身)向上,找到「离它最近、且对 appId 有任意显式模块配置」的组作为 + * 该应用的唯一权限来源;该模块被许可,当且仅当这个来源组对它自身有显式 {@code sys_group_module_perm} 记录。 + * 这样: + * ① 本组有配置(如英语组只勾模块8)→ 本组即来源,只返回本组勾选的模块,不再合并学校全部模块; + * ② 本组无配置(如英语组为空)→ 取最近的有配置祖先(如教师组)为来源,不再越级到更远的学校组; + * ③ 来源组的模块集合即最终集合,更上层同应用模块(如学校组多勾的模块8)一律不合并。 + * + *

学校截止:祖先链只查到第一个学校组(含)为止,不跨学校到教育局/根。 + * + *

其余:归属组需已开通该模块所属应用(双保险,防越权)。 */ private boolean hasUserModulePermission(Integer moduleId, Integer appId, Set selfGroupIds, Map> chainMap, Map> groupToApps, Map> modulePermMap, - Map allGroupMap) { + Map allGroupMap, + Map> groupConfiguredApps) { Map permMap = modulePermMap.get(moduleId); - if (permMap == null || permMap.isEmpty()) { - return false; - } for (Integer selfGroupId : selfGroupIds) { Set openedApps = groupToApps.get(selfGroupId); if (openedApps == null || !openedApps.contains(appId)) { @@ -891,33 +942,14 @@ public class GroupAuthzService implements IGroupAuthzService { if (chain == null || chain.isEmpty()) { continue; } - // 第一段:本组显式设置优先(记录存在=true,完全按本组来,不继承不向上) - SysGroupModulePerm selfPerm = permMap.get(selfGroupId); - if (selfPerm != null) { - return true; - } - // 本组无记录 → 第二段:继承查找。 - // 如果「本组本身就是学校组」:学校组已经是继承上限,学校组之上(区域/教育局/根)不再允许查找,直接跳过本段。 - // 否则(本组是班级/年级/教师/学生等学校内子组):沿祖先向上,直到包含第一个学校组为止,出了学校组立刻break,不跨学校。 - SysGroup selfG = allGroupMap.get(selfGroupId); - boolean selfIsSchool = selfG != null && selfG.getType() == GroupTypeEnum.SCHOOL_GROUP; - if (selfIsSchool) { + // 就近原则:取离本组最近、对该应用有显式配置的组作为唯一权限来源 + Integer authority = findNearestAuthorityGroup(selfGroupId, chain, appId, groupConfiguredApps, allGroupMap); + if (authority == null) { continue; } - for (Integer ancestorId : chain) { - if (Objects.equals(ancestorId, selfGroupId)) { - continue; - } - SysGroup g = allGroupMap.get(ancestorId); - SysGroupModulePerm perm = permMap.get(ancestorId); - boolean isSchool = g != null && g.getType() == GroupTypeEnum.SCHOOL_GROUP; - if (perm != null && Boolean.TRUE.equals(perm.getInheritFlag())) { - return true; - } - // 用户要求:向上查找只查到学校组这一层,包含学校组本身(上面已判断学校组的perm),之后不再跨学校 - if (isSchool) { - break; - } + // 权限来源组对该模块有显式记录 → 许可;否则(来源组未勾此模块)不许可 + if (permMap != null && permMap.get(authority) != null) { + return true; } } return false; @@ -1135,6 +1167,7 @@ public class GroupAuthzService implements IGroupAuthzService { if (module == null) { return false; } + Integer appId = module.getAppId(); // AI-FIX(角色授权): 用户经「启用」角色获得该模块授权即通过,不要求组开通模块所属应用;禁用角色已在收集时过滤 if (collectEnabledRoleModuleIds(userId).contains(moduleId)) { return true; @@ -1149,18 +1182,20 @@ public class GroupAuthzService implements IGroupAuthzService { for (SysGroup g : sysGroupRepository.findAll()) { allGroupMap.put(g.getId(), g); } - // 3. 前置校验:所属组必须已开通模块所属应用(最终权限 = 继承的模块权限 ∩ 已开通应用); + // 3. 前置校验:所属组必须已开通模块所属应用(最终权限 = 就近来源模块权限 ∩ 已开通应用); // 通过校验的组构建祖先链(含自身),并收集祖先链并集用于一次批量 IN 查询 Map> chainMap = new HashMap<>(); Set allChainIds = new LinkedHashSet<>(); + Set selfGroupIds = new LinkedHashSet<>(); for (SysUserGroupRel rel : userRels) { Integer groupId = rel.getGroupId(); if (groupId == null || chainMap.containsKey(groupId)) { continue; } - if (!sysGroupAppRelRepository.existsByGroupIdAndAppId(groupId, module.getAppId())) { + if (!sysGroupAppRelRepository.existsByGroupIdAndAppId(groupId, appId)) { continue; } + selfGroupIds.add(groupId); List chain = ancestorChain(groupId, allGroupMap); chainMap.put(groupId, chain); allChainIds.addAll(chain); @@ -1168,30 +1203,38 @@ public class GroupAuthzService implements IGroupAuthzService { if (chainMap.isEmpty()) { return false; } - // 4. 一次批量查询目标模块在所有祖先链节点上的权限记录 - Map permMap = new HashMap<>(); - List perms = sysGroupModulePermRepository.findByModuleIdAndGroupIdIn(moduleId, - new ArrayList<>(allChainIds)); - for (SysGroupModulePerm perm : perms) { - permMap.put(perm.getGroupId(), perm); + // 4. 一次批量查询「该应用下所有模块」在全部祖先链节点上的权限记录 + // (要判定就近来源,需要知道链上哪些组对该应用有显式配置,不能只看单模块) + List appModules = appModuleRepository.findByAppIdIn(Collections.singletonList(appId)); + Set appModuleIds = new LinkedHashSet<>(); + for (AppModule m : appModules) { + appModuleIds.add(m.getId()); } - if (permMap.isEmpty()) { - return false; - } - // 5. 判定:自身显式勾选直接通过;祖先勾选且开启继承则子孙生效 - for (Map.Entry> entry : chainMap.entrySet()) { - Integer selfGroupId = entry.getKey(); - for (Integer ancestorId : entry.getValue()) { - SysGroupModulePerm perm = permMap.get(ancestorId); - if (perm == null) { + Map> modulePermMap = new HashMap<>(); + Map> groupConfiguredApps = new HashMap<>(); + if (!appModuleIds.isEmpty() && !allChainIds.isEmpty()) { + List perms = sysGroupModulePermRepository.findByModuleIdInAndGroupIdIn( + new ArrayList<>(appModuleIds), new ArrayList<>(allChainIds)); + for (SysGroupModulePerm perm : perms) { + if (perm.getModuleId() == null || perm.getGroupId() == null) { continue; } - if (Objects.equals(ancestorId, selfGroupId)) { - return true; - } - if (Boolean.TRUE.equals(perm.getInheritFlag())) { - return true; - } + modulePermMap.computeIfAbsent(perm.getModuleId(), k -> new HashMap<>()) + .put(perm.getGroupId(), perm); + groupConfiguredApps.computeIfAbsent(perm.getGroupId(), k -> new HashSet<>()).add(appId); + } + } + // 5. 就近原则判定:对每个归属组,取离它最近的、对该应用有显式配置的组为唯一权限来源; + // 该来源组对目标模块有记录即通过(多个归属组取并集)。 + for (Integer selfGroupId : selfGroupIds) { + List chain = chainMap.get(selfGroupId); + Integer authority = findNearestAuthorityGroup(selfGroupId, chain, appId, groupConfiguredApps, allGroupMap); + if (authority == null) { + continue; + } + Map pm = modulePermMap.get(moduleId); + if (pm != null && pm.get(authority) != null) { + return true; } } return false;