Browse Source
将 PermissionServiceImpl(双职责、无接口、吞异常、混合返回类型)重构为深模块: - PermissionResolver 接口:resolve(userId) + visibleMenuTree(userId) - PermissionGrant 不可变值对象:结构化分离 permCodes / roleCodes,asAuthorities() 幂等归一 ROLE_ 前缀 - resolve 变纯函数:不装载 DataVisibilityContext,由过滤器负责 - fail-closed:删除 try/catch 吞异常,解析失败即请求失败(ADR-0006) - 查询链 locality:resolve 与 visibleMenuTree 共用 userRolesOf + authorizedMenus - JwtAuthenticationFilter / SystemController 切换到新接口 - DataScopeIntegrationTest 迁移到 resolve(),新增 6 个权限码并集场景master
7 changed files with 357 additions and 149 deletions
@ -0,0 +1,67 @@ |
|||
package com.crm.auth.security; |
|||
|
|||
import com.crm.base.security.DataVisibility; |
|||
import com.crm.base.security.DataVisibilityContext; |
|||
import org.springframework.security.core.authority.SimpleGrantedAuthority; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.Set; |
|||
|
|||
/** |
|||
* 权限解析结果:一次 resolve 拿齐一个用户的三份授权事实(ADR-0011)。 |
|||
* |
|||
* <p>不可变。纯取值对象——装载 {@link DataVisibilityContext} 是请求生命周期所有者 |
|||
* (JWT 过滤器)的职责,本对象不产生副作用。</p> |
|||
* |
|||
* <ul> |
|||
* <li>{@link #getVisibility()} 数据可见性(部门集合 + 档位 + 子树展开)</li> |
|||
* <li>{@link #getPermCodes()} 权限码并集(停用权限点已全局断路,不进并集)</li> |
|||
* <li>{@link #getRoleCodes()} 角色编码集合</li> |
|||
* </ul> |
|||
*/ |
|||
public final class PermissionGrant { |
|||
|
|||
/** Spring Security 角色 authority 前缀(hasRole('X') 实际匹配 ROLE_X) */ |
|||
private static final String ROLE_PREFIX = "ROLE_"; |
|||
|
|||
private final DataVisibility visibility; |
|||
private final Set<String> permCodes; |
|||
private final Set<String> roleCodes; |
|||
|
|||
public PermissionGrant(DataVisibility visibility, Set<String> permCodes, Set<String> roleCodes) { |
|||
this.visibility = visibility; |
|||
this.permCodes = permCodes == null ? Set.of() : Set.copyOf(permCodes); |
|||
this.roleCodes = roleCodes == null ? Set.of() : Set.copyOf(roleCodes); |
|||
} |
|||
|
|||
public DataVisibility getVisibility() { |
|||
return visibility; |
|||
} |
|||
|
|||
/** 权限码并集:button 节点 perms 的去重集合,已排除停用权限点 */ |
|||
public Set<String> getPermCodes() { |
|||
return permCodes; |
|||
} |
|||
|
|||
/** 角色编码集合(原样,不带 ROLE_ 前缀约定) */ |
|||
public Set<String> getRoleCodes() { |
|||
return roleCodes; |
|||
} |
|||
|
|||
/** |
|||
* 合成 Spring Security authorities:权限码原样注入;角色编码幂等归一到 ROLE_ 前缀 |
|||
* (已带前缀的不重复加),使 hasAuthority / hasRole 各自按约定匹配。 |
|||
* 前缀约定由本模块保证,调用方与 DB 数据无需理解它。 |
|||
*/ |
|||
public List<SimpleGrantedAuthority> asAuthorities() { |
|||
List<SimpleGrantedAuthority> authorities = new ArrayList<>(permCodes.size() + roleCodes.size()); |
|||
permCodes.forEach(perm -> authorities.add(new SimpleGrantedAuthority(perm))); |
|||
roleCodes.forEach(role -> authorities.add(new SimpleGrantedAuthority(normalizeRole(role)))); |
|||
return List.copyOf(authorities); |
|||
} |
|||
|
|||
private static String normalizeRole(String roleCode) { |
|||
return roleCode.startsWith(ROLE_PREFIX) ? roleCode : ROLE_PREFIX + roleCode; |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
package com.crm.auth.security; |
|||
|
|||
import com.crm.auth.domain.entity.SysMenu; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 权限解析引擎:每请求把一个用户的全部授权事实一次算清(ADR-0011)。 |
|||
* |
|||
* <p>深模块:调用方只见两个方法,「用户 → 角色 → 授权 → 资源」的查询链、 |
|||
* 档位取舍、子树展开、权限码并集的断路规则全部藏在实现里。</p> |
|||
* |
|||
* <p>fail-closed(ADR-0006):解析失败即抛异常,由请求入口拒绝该请求; |
|||
* 不存在「部分成功」的第三状态。</p> |
|||
*/ |
|||
public interface PermissionResolver { |
|||
|
|||
/** |
|||
* 解析用户的数据可见性与全部授权码。纯取值——不装载任何线程上下文, |
|||
* 装载 {@link com.crm.base.security.DataVisibilityContext} 由调用方按请求生命周期负责。 |
|||
* |
|||
* @param userId 本地用户 ID |
|||
* @return 可见性 + 权限码并集 + 角色编码;无角色用户返回最窄档可见性与空授权集合 |
|||
*/ |
|||
PermissionGrant resolve(Long userId); |
|||
|
|||
/** |
|||
* 用户可见菜单树(catalog/menu 节点,按角色授权过滤、排除 button、按 sort 排序)。 |
|||
* 登录后渲染侧边栏用,不参与每请求解析。 |
|||
* |
|||
* @param userId 本地用户 ID |
|||
* @return 组好父子关系的菜单树;无任何授权时返回空列表 |
|||
*/ |
|||
List<SysMenu> visibleMenuTree(Long userId); |
|||
} |
|||
@ -0,0 +1,139 @@ |
|||
package com.crm.auth.security; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import cn.hutool.core.util.StrUtil; |
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|||
import com.crm.auth.domain.entity.AuthUser; |
|||
import com.crm.auth.domain.entity.SysMenu; |
|||
import com.crm.auth.domain.entity.SysRole; |
|||
import com.crm.auth.domain.entity.SysRoleMenu; |
|||
import com.crm.auth.domain.entity.SysUserDept; |
|||
import com.crm.auth.domain.entity.SysUserRole; |
|||
import com.crm.auth.domain.enums.MenuType; |
|||
import com.crm.auth.mapper.AuthUserMapper; |
|||
import com.crm.auth.mapper.SysMenuMapper; |
|||
import com.crm.auth.mapper.SysRoleMapper; |
|||
import com.crm.auth.mapper.SysRoleMenuMapper; |
|||
import com.crm.auth.mapper.SysUserDeptMapper; |
|||
import com.crm.auth.mapper.SysUserRoleMapper; |
|||
import com.crm.auth.service.DeptTreeCache; |
|||
import com.crm.base.security.DataScopeLevel; |
|||
import com.crm.base.security.DataVisibility; |
|||
import com.crm.base.utils.TreeUtils; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.util.Collections; |
|||
import java.util.Comparator; |
|||
import java.util.LinkedHashSet; |
|||
import java.util.List; |
|||
import java.util.Objects; |
|||
import java.util.Set; |
|||
import java.util.stream.Collectors; |
|||
|
|||
/** |
|||
* 权限解析引擎实现:数据可见性、权限码并集、可见菜单树共用同一条 |
|||
* 「用户 → 角色 → 授权 → 资源」查询链(ADR-0011)。 |
|||
*/ |
|||
@Slf4j |
|||
@Component |
|||
@RequiredArgsConstructor |
|||
public class PermissionResolverImpl implements PermissionResolver { |
|||
|
|||
private final SysUserRoleMapper sysUserRoleMapper; |
|||
private final SysRoleMapper sysRoleMapper; |
|||
private final SysRoleMenuMapper sysRoleMenuMapper; |
|||
private final SysMenuMapper sysMenuMapper; |
|||
private final AuthUserMapper authUserMapper; |
|||
private final DeptTreeCache deptTreeCache; |
|||
private final SysUserDeptMapper sysUserDeptMapper; |
|||
|
|||
@Override |
|||
public PermissionGrant resolve(Long userId) { |
|||
// ── 数据可见性:无角色取最窄档(仅本人),多角色取最宽档(ADR-0008)──
|
|||
List<SysUserRole> userRoles = userRolesOf(userId); |
|||
DataScopeLevel widest = DataScopeLevel.SELF; |
|||
List<SysRole> roles = Collections.emptyList(); |
|||
if (CollUtil.isNotEmpty(userRoles)) { |
|||
roles = sysRoleMapper.selectBatchIds(roleIdsOf(userRoles)); |
|||
for (SysRole role : roles) { |
|||
// 未配档位的角色按最窄档处理,不放宽任何人的可见范围
|
|||
if (role.getDataScope() == null) continue; |
|||
DataScopeLevel level = DataScopeLevel.fromCode(role.getDataScope()); |
|||
if (level.getCode() > widest.getCode()) { |
|||
widest = level; |
|||
} |
|||
} |
|||
} |
|||
AuthUser user = authUserMapper.selectById(userId); |
|||
Long primaryDeptId = user != null ? user.getDeptId() : null; |
|||
// 部门集合 = 主部门 + 兼职部门并集,集合内地位平等(ADR-0005);兼职关系每请求实时查
|
|||
Set<Long> deptIds = new LinkedHashSet<>(); |
|||
if (primaryDeptId != null) { |
|||
deptIds.add(primaryDeptId); |
|||
} |
|||
sysUserDeptMapper.selectList( |
|||
new LambdaQueryWrapper<SysUserDept>().eq(SysUserDept::getUserId, userId)) |
|||
.forEach(ud -> deptIds.add(ud.getDeptId())); |
|||
List<Long> expandedDeptIds = null; |
|||
if (widest == DataScopeLevel.DEPT_AND_CHILDREN) { |
|||
// 子树展开在缓存树上完成,多部门共享同一棵树;无部门可展开时为空集合,即一律不可见
|
|||
expandedDeptIds = deptIds.isEmpty() ? List.of() : deptTreeCache.expandWithChildren(deptIds); |
|||
} |
|||
DataVisibility visibility = new DataVisibility( |
|||
userId, primaryDeptId, List.copyOf(deptIds), widest, expandedDeptIds); |
|||
|
|||
// ── 权限码并集(ADR-0011):button 且 status=enabled 的 perms,去重 ──
|
|||
Set<String> permCodes = CollUtil.isEmpty(userRoles) |
|||
? Set.of() |
|||
: authorizedMenus(roleIdsOf(userRoles)).stream() |
|||
.filter(m -> m.getMenuType() != null && m.getMenuType() == MenuType.BUTTON.getCode()) |
|||
.filter(m -> "enabled".equals(m.getStatus())) |
|||
.map(SysMenu::getPerms) |
|||
.filter(StrUtil::isNotBlank) |
|||
.collect(Collectors.toCollection(LinkedHashSet::new)); |
|||
|
|||
// ── 角色编码(原样持有,ROLE_ 前缀归一由 PermissionGrant 负责)──
|
|||
Set<String> roleCodes = roles.stream() |
|||
.map(SysRole::getRoleCode) |
|||
.filter(Objects::nonNull) |
|||
.collect(Collectors.toCollection(LinkedHashSet::new)); |
|||
|
|||
return new PermissionGrant(visibility, permCodes, roleCodes); |
|||
} |
|||
|
|||
@Override |
|||
public List<SysMenu> visibleMenuTree(Long userId) { |
|||
List<SysUserRole> userRoles = userRolesOf(userId); |
|||
if (CollUtil.isEmpty(userRoles)) return Collections.emptyList(); |
|||
List<SysMenu> visibleMenus = authorizedMenus(roleIdsOf(userRoles)).stream() |
|||
.filter(m -> Boolean.TRUE.equals(m.getVisible())) |
|||
.filter(m -> m.getMenuType() == null || m.getMenuType() != MenuType.BUTTON.getCode()) |
|||
.sorted(Comparator.comparingInt(m -> m.getSort() != null ? m.getSort() : 0)) |
|||
.collect(Collectors.toList()); |
|||
return TreeUtils.buildTree(visibleMenus, SysMenu::getId, SysMenu::getParentId, SysMenu::setChildren); |
|||
} |
|||
|
|||
// ==================== 共享查询链 ====================
|
|||
|
|||
private List<SysUserRole> userRolesOf(Long userId) { |
|||
return sysUserRoleMapper.selectList( |
|||
new LambdaQueryWrapper<SysUserRole>().eq(SysUserRole::getUserId, userId)); |
|||
} |
|||
|
|||
private static List<Long> roleIdsOf(List<SysUserRole> userRoles) { |
|||
return userRoles.stream().map(SysUserRole::getRoleId).distinct().collect(Collectors.toList()); |
|||
} |
|||
|
|||
/** 角色授权的资源节点全集(含 button / menu / catalog),过滤规则由调用方决定 */ |
|||
private List<SysMenu> authorizedMenus(List<Long> roleIds) { |
|||
List<SysRoleMenu> roleMenus = sysRoleMenuMapper.selectList( |
|||
new LambdaQueryWrapper<SysRoleMenu>().in(SysRoleMenu::getRoleId, roleIds)); |
|||
if (CollUtil.isEmpty(roleMenus)) { |
|||
return Collections.emptyList(); |
|||
} |
|||
Set<Long> menuIds = roleMenus.stream().map(SysRoleMenu::getMenuId).collect(Collectors.toSet()); |
|||
return sysMenuMapper.selectBatchIds(menuIds); |
|||
} |
|||
} |
|||
@ -1,123 +0,0 @@ |
|||
package com.crm.auth.service.impl; |
|||
|
|||
import cn.hutool.core.collection.CollUtil; |
|||
import cn.hutool.core.util.StrUtil; |
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|||
import com.crm.auth.domain.entity.*; |
|||
import com.crm.auth.domain.enums.MenuType; |
|||
import com.crm.auth.mapper.*; |
|||
import com.crm.auth.service.DeptTreeCache; |
|||
import com.crm.base.security.DataScopeLevel; |
|||
import com.crm.base.security.DataVisibility; |
|||
import com.crm.base.security.DataVisibilityContext; |
|||
import com.crm.base.utils.TreeUtils; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import java.util.*; |
|||
import java.util.stream.Collectors; |
|||
|
|||
@Slf4j |
|||
@Service |
|||
@RequiredArgsConstructor |
|||
public class PermissionServiceImpl { |
|||
|
|||
private final SysUserRoleMapper sysUserRoleMapper; |
|||
private final SysRoleMapper sysRoleMapper; |
|||
private final SysRoleMenuMapper sysRoleMenuMapper; |
|||
private final SysMenuMapper sysMenuMapper; |
|||
private final AuthUserMapper authUserMapper; |
|||
private final DeptTreeCache deptTreeCache; |
|||
private final SysUserDeptMapper sysUserDeptMapper; |
|||
|
|||
/** |
|||
* 装入当前用户的数据可见性范围,并返回其角色编码集合。 |
|||
* <p>角色编码由 JWT 过滤器填充为 Spring Security authority,使 {@code @PreAuthorize(hasRole(...))} |
|||
* 生效(ADR-0008);无角色返回空集。</p> |
|||
*/ |
|||
public List<String> initDataScopeContext(Long userId) { |
|||
List<SysUserRole> userRoles = sysUserRoleMapper.selectList( |
|||
new LambdaQueryWrapper<SysUserRole>().eq(SysUserRole::getUserId, userId)); |
|||
// 初值取最窄档(仅本人),无角色用户不会被放行全部数据;多角色取最宽档
|
|||
DataScopeLevel widest = DataScopeLevel.SELF; |
|||
List<SysRole> roles = Collections.emptyList(); |
|||
if (CollUtil.isNotEmpty(userRoles)) { |
|||
List<Long> roleIds = userRoles.stream().map(SysUserRole::getRoleId).collect(Collectors.toList()); |
|||
roles = sysRoleMapper.selectBatchIds(roleIds); |
|||
for (SysRole role : roles) { |
|||
// 未配档位的角色按最窄档处理,不放宽任何人的可见范围
|
|||
if (role.getDataScope() == null) continue; |
|||
DataScopeLevel level = DataScopeLevel.fromCode(role.getDataScope()); |
|||
if (level.getCode() > widest.getCode()) { |
|||
widest = level; |
|||
} |
|||
} |
|||
} |
|||
AuthUser user = authUserMapper.selectById(userId); |
|||
Long primaryDeptId = user != null ? user.getDeptId() : null; |
|||
// 部门集合 = 主部门 + 兼职部门并集,集合内地位平等(ADR 0005);兼职关系每请求实时查
|
|||
Set<Long> deptIds = new LinkedHashSet<>(); |
|||
if (primaryDeptId != null) { |
|||
deptIds.add(primaryDeptId); |
|||
} |
|||
sysUserDeptMapper.selectList( |
|||
new LambdaQueryWrapper<SysUserDept>().eq(SysUserDept::getUserId, userId)) |
|||
.forEach(ud -> deptIds.add(ud.getDeptId())); |
|||
List<Long> expandedDeptIds = null; |
|||
if (widest == DataScopeLevel.DEPT_AND_CHILDREN) { |
|||
// 子树展开在缓存树上完成,多部门共享同一棵树(工单 05);无部门可展开时为空集合,即一律不可见
|
|||
expandedDeptIds = deptIds.isEmpty() ? List.of() : deptTreeCache.expandWithChildren(deptIds); |
|||
} |
|||
DataVisibilityContext.load( |
|||
new DataVisibility(userId, primaryDeptId, List.copyOf(deptIds), widest, expandedDeptIds)); |
|||
|
|||
// ── 权限码并集(ADR-0011):收集用户所有角色授权的 button 节点 perms ──
|
|||
List<String> authorities = new ArrayList<>(); |
|||
if (CollUtil.isNotEmpty(userRoles)) { |
|||
try { |
|||
Set<Long> roleIds = userRoles.stream().map(SysUserRole::getRoleId).collect(Collectors.toSet()); |
|||
List<SysRoleMenu> allRoleMenus = sysRoleMenuMapper.selectList( |
|||
new LambdaQueryWrapper<SysRoleMenu>().in(SysRoleMenu::getRoleId, roleIds)); |
|||
if (CollUtil.isNotEmpty(allRoleMenus)) { |
|||
Set<Long> menuIds = allRoleMenus.stream() |
|||
.map(SysRoleMenu::getMenuId).collect(Collectors.toSet()); |
|||
List<SysMenu> menus = sysMenuMapper.selectBatchIds(menuIds); |
|||
menus.stream() |
|||
.filter(m -> m.getMenuType() != null && m.getMenuType() == MenuType.BUTTON.getCode()) |
|||
.filter(m -> "enabled".equals(m.getStatus())) |
|||
.map(SysMenu::getPerms) |
|||
.filter(StrUtil::isNotBlank) |
|||
.distinct() |
|||
.forEach(authorities::add); |
|||
} |
|||
} catch (Exception e) { |
|||
log.debug("权限码并集查询失败(可能 sys_menu/sys_role_menu 表尚未创建),跳过", e); |
|||
} |
|||
} |
|||
// 角色编码追加在权限码之后(两者同为 Spring Security authority,hasRole/hasAuthority 各自匹配)
|
|||
roles.stream() |
|||
.map(SysRole::getRoleCode) |
|||
.filter(Objects::nonNull) |
|||
.forEach(authorities::add); |
|||
return authorities; |
|||
} |
|||
|
|||
public List<SysMenu> getMenuTree(Long userId) { |
|||
List<SysUserRole> userRoles = sysUserRoleMapper.selectList( |
|||
new LambdaQueryWrapper<SysUserRole>().eq(SysUserRole::getUserId, userId)); |
|||
if (CollUtil.isEmpty(userRoles)) return Collections.emptyList(); |
|||
List<Long> roleIds = userRoles.stream().map(SysUserRole::getRoleId).collect(Collectors.toList()); |
|||
List<SysRoleMenu> roleMenus = sysRoleMenuMapper.selectList( |
|||
new LambdaQueryWrapper<SysRoleMenu>().in(SysRoleMenu::getRoleId, roleIds)); |
|||
if (CollUtil.isEmpty(roleMenus)) return Collections.emptyList(); |
|||
Set<Long> menuIds = roleMenus.stream().map(SysRoleMenu::getMenuId).collect(Collectors.toSet()); |
|||
List<SysMenu> allMenus = sysMenuMapper.selectBatchIds(menuIds); |
|||
List<SysMenu> visibleMenus = allMenus.stream() |
|||
.filter(m -> Boolean.TRUE.equals(m.getVisible())) |
|||
.filter(m -> m.getMenuType() != MenuType.BUTTON.getCode()) |
|||
.sorted(Comparator.comparingInt(m -> m.getSort() != null ? m.getSort() : 0)) |
|||
.collect(Collectors.toList()); |
|||
return TreeUtils.buildTree(visibleMenus, SysMenu::getId, SysMenu::getParentId, SysMenu::setChildren); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue