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