Browse Source

refactor(auth): 权限解析收成深模块 PermissionResolver

将 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
luoweijian 1 month ago
parent
commit
c877f0d6c4
  1. 6
      crm-auth/src/main/java/com/crm/auth/controller/SystemController.java
  2. 17
      crm-auth/src/main/java/com/crm/auth/security/JwtAuthenticationFilter.java
  3. 67
      crm-auth/src/main/java/com/crm/auth/security/PermissionGrant.java
  4. 35
      crm-auth/src/main/java/com/crm/auth/security/PermissionResolver.java
  5. 139
      crm-auth/src/main/java/com/crm/auth/security/PermissionResolverImpl.java
  6. 123
      crm-auth/src/main/java/com/crm/auth/service/impl/PermissionServiceImpl.java
  7. 119
      crm-auth/src/test/java/com/crm/auth/security/scope/DataScopeIntegrationTest.java

6
crm-auth/src/main/java/com/crm/auth/controller/SystemController.java

@ -14,7 +14,7 @@ import com.crm.auth.service.IAuthUserService;
import com.crm.auth.service.ISysDeptService;
import com.crm.auth.service.ISysMenuService;
import com.crm.auth.service.ISysRoleService;
import com.crm.auth.service.impl.PermissionServiceImpl;
import com.crm.auth.security.PermissionResolver;
import com.crm.base.domain.result.PageResult;
import com.crm.base.domain.result.Result;
import com.crm.base.security.SecurityUtils;
@ -35,7 +35,7 @@ public class SystemController {
private final ISysMenuService sysMenuService;
private final ISysDeptService sysDeptService;
private final IAuthUserService authUserService;
private final PermissionServiceImpl permissionService;
private final PermissionResolver permissionResolver;
private final SysRoleMenuMapper sysRoleMenuMapper;
private final SysUserRoleMapper sysUserRoleMapper;
@ -44,7 +44,7 @@ public class SystemController {
@GetMapping("/menus/tree")
public Result<List<SysMenu>> menuTree() {
Long userId = Long.valueOf(SecurityUtils.getRequiredUserId());
return Result.success(permissionService.getMenuTree(userId));
return Result.success(permissionResolver.visibleMenuTree(userId));
}
@PostMapping("/menus/save")

17
crm-auth/src/main/java/com/crm/auth/security/JwtAuthenticationFilter.java

@ -2,7 +2,6 @@ package com.crm.auth.security;
import cn.hutool.core.util.StrUtil;
import com.crm.auth.constant.AuthConstants;
import com.crm.auth.service.impl.PermissionServiceImpl;
import com.crm.base.security.DataVisibilityContext;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
@ -11,14 +10,12 @@ import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
/**
* JWT 认证过滤器 Authorization: Bearer {token} 中解析登录态
@ -30,7 +27,7 @@ import java.util.List;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final TokenService tokenService;
private final PermissionServiceImpl permissionService;
private final PermissionResolver permissionResolver;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
@ -40,14 +37,12 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
if (token != null && SecurityContextHolder.getContext().getAuthentication() == null) {
AuthLoginUser loginUser = tokenService.verifyToken(token);
if (loginUser != null) {
// 装入数据可见性范围并取角色编码集合:装不上就让请求失败(ADR 0006)
List<String> roleCodes = permissionService.initDataScopeContext(Long.valueOf(loginUser.getUserId()));
// 角色编码 → Spring Security authority,使 @PreAuthorize(hasRole(...)) 生效(ADR-0008)
List<SimpleGrantedAuthority> authorities = roleCodes.stream()
.map(SimpleGrantedAuthority::new)
.toList();
// 权限解析引擎一次算清:装不上可见性就让请求失败(fail-closed,ADR-0006)
PermissionGrant grant = permissionResolver.resolve(Long.valueOf(loginUser.getUserId()));
DataVisibilityContext.load(grant.getVisibility());
// 权限码并集 + 角色编码 → Spring Security authority(ADR-0008 / ADR-0011)
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(loginUser, null, authorities);
new UsernamePasswordAuthenticationToken(loginUser, null, grant.asAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {

67
crm-auth/src/main/java/com/crm/auth/security/PermissionGrant.java

@ -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;
}
}

35
crm-auth/src/main/java/com/crm/auth/security/PermissionResolver.java

@ -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-closedADR-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);
}

139
crm-auth/src/main/java/com/crm/auth/security/PermissionResolverImpl.java

@ -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);
}
}

123
crm-auth/src/main/java/com/crm/auth/service/impl/PermissionServiceImpl.java

@ -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);
}
}

119
crm-auth/src/test/java/com/crm/auth/security/scope/DataScopeIntegrationTest.java

@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils;
import com.crm.auth.domain.entity.AuthUser;
import com.crm.auth.domain.entity.SysDept;
import com.crm.auth.domain.entity.SysMenu;
import com.crm.auth.domain.entity.SysUserDept;
import com.crm.auth.mapper.AuthUserMapper;
import com.crm.auth.mapper.SysDeptMapper;
@ -15,8 +16,10 @@ import com.crm.auth.mapper.SysUserDeptMapper;
import com.crm.auth.mapper.SysUserRoleMapper;
import com.crm.auth.security.DataScopeInterceptor;
import com.crm.auth.security.DataScopeTables;
import com.crm.auth.security.PermissionGrant;
import com.crm.auth.security.PermissionResolver;
import com.crm.auth.security.PermissionResolverImpl;
import com.crm.auth.service.DeptTreeCache;
import com.crm.auth.service.impl.PermissionServiceImpl;
import com.crm.base.annotation.DataScope;
import com.crm.base.config.MetaObjectFillHandler;
import com.crm.base.security.DataVisibilityContext;
@ -37,6 +40,7 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.sql.Connection;
import java.sql.Statement;
@ -50,9 +54,9 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* 权限上下文 可见行集成缝工单 01 主缝
*
* <p>给定用户 + 角色档位 + 主部门 + 部门树经真实的 {@link PermissionServiceImpl#initDataScopeContext}
* <p>给定用户 + 角色档位 + 主部门 + 部门树经真实的 {@link PermissionResolver#resolve}
* 与真实的 Mapper 查询H2 MySQL 模式 + {@link DataScopeInterceptor}断言测试专用受控实体
* {@link TestOwnedData} 的可见行集合不断言生成的 SQL 字符串</p>
* {@link TestOwnedData} 的可见行集合与权限码并集不断言生成的 SQL 字符串</p>
*
* <p>固定数据集</p>
* <pre>
@ -69,7 +73,7 @@ class DataScopeIntegrationTest {
private static SqlSessionFactory sqlSessionFactory;
private SqlSession session;
private PermissionServiceImpl permissionService;
private PermissionResolver resolver;
private DeptTreeCache deptTreeCache;
private TestOwnedDataMapper testDataMapper;
private SysDeptMapper deptMapper;
@ -136,7 +140,7 @@ class DataScopeIntegrationTest {
// 非自动提交 + AfterEach 回滚:插入类用例不污染共享的静态数据集
session = sqlSessionFactory.openSession();
deptTreeCache = new DeptTreeCache(session.getMapper(SysDeptMapper.class));
permissionService = new PermissionServiceImpl(
resolver = new PermissionResolverImpl(
session.getMapper(SysUserRoleMapper.class),
session.getMapper(SysRoleMapper.class),
session.getMapper(SysRoleMenuMapper.class),
@ -159,10 +163,15 @@ class DataScopeIntegrationTest {
}
private List<Long> visibleRowIds(long userId) {
permissionService.initDataScopeContext(userId);
loadVisibility(userId);
return testDataMapper.selectList(null).stream().map(TestOwnedData::getId).toList();
}
/** 走新缝:resolve 是纯函数,装载上下文由调用方(测试模拟过滤器)负责 */
private void loadVisibility(long userId) {
DataVisibilityContext.load(resolver.resolve(userId).getVisibility());
}
@Test
@DisplayName("档位1 仅本人:只可见归属人是自己的行")
void scopeOne_seesOnlyOwnRows() {
@ -214,7 +223,7 @@ class DataScopeIntegrationTest {
@Test
@DisplayName("未标注 @DataScope 的表不受过滤影响")
void unannotatedTable_isNotFiltered() {
permissionService.initDataScopeContext(2L); // 档位2 的用户
loadVisibility(2L); // 档位2 的用户
assertThat(deptMapper.selectList(null)).hasSize(5);
}
@ -277,9 +286,9 @@ class DataScopeIntegrationTest {
@Test
@DisplayName("部门树缓存:第二次权限计算不再触发部门全表查询(多部门共享同一棵树)")
void deptTree_isCached_noRepeatedFullTableQuery() {
permissionService.initDataScopeContext(3L); // 档位3,首次建树
loadVisibility(3L); // 档位3,首次建树
DeptQueryCounter.COUNT.set(0);
permissionService.initDataScopeContext(22L); // 另一档位3用户(多部门),命中缓存树
loadVisibility(22L); // 另一档位3用户(多部门),命中缓存树
assertThat(DeptQueryCounter.COUNT.get())
.as("第二次权限计算的 sys_dept 全表查询次数")
.isZero();
@ -333,7 +342,7 @@ class DataScopeIntegrationTest {
@Test
@DisplayName("插入自动归属:owner 填当前用户,dept 填主部门(取值源是数据权限上下文)")
void insert_autoFillsOwnerAndDeptFromContext() {
permissionService.initDataScopeContext(1L); // 用户1:主部门 10
loadVisibility(1L); // 用户1:主部门 10
TestOwnedData row = new TestOwnedData();
row.setId(3001L);
row.setTitle("自动归属");
@ -345,7 +354,7 @@ class DataScopeIntegrationTest {
@Test
@DisplayName("插入显式赋值:业务代码已赋 owner/dept 时不被自动填充覆盖")
void insert_explicitValues_notOverridden() {
permissionService.initDataScopeContext(1L);
loadVisibility(1L);
TestOwnedData row = new TestOwnedData();
row.setId(3002L);
row.setTitle("显式归属");
@ -359,7 +368,7 @@ class DataScopeIntegrationTest {
@Test
@DisplayName("无主部门用户插入:owner 照填,dept 保持 null(不从兼职集合里挑)")
void insert_withoutPrimaryDept_leavesDeptNull() {
permissionService.initDataScopeContext(23L); // 用户23:无主部门,仅兼职 11
loadVisibility(23L); // 用户23:无主部门,仅兼职 11
TestOwnedData row = new TestOwnedData();
row.setId(3003L);
row.setTitle("无主部门");
@ -379,6 +388,63 @@ class DataScopeIntegrationTest {
assertThat(row.getDeptId()).isNull();
}
// ==================== 权限码并集(ADR-0011) ====================
@Test
@DisplayName("权限码并集:多角色取并集,停用权限点全局断路,menu 节点不进并集")
void permUnion_multiRole_enabledButtonsOnly() {
PermissionGrant grant = resolver.resolve(5L); // 角色1+2:授权了 301,302,303,304
assertThat(grant.getPermCodes())
.as("302 停用断路、303 是 menu 节点,均不进并集")
.containsExactlyInAnyOrder("crm:user:list", "crm:order:list");
assertThat(grant.getRoleCodes()).containsExactlyInAnyOrder("SELF", "DEPT");
}
@Test
@DisplayName("停用权限点断路:已授权但 status=disabled 的 perms 不进并集")
void permUnion_disabledButton_cutOff() {
PermissionGrant grant = resolver.resolve(1L); // 角色1:授权 301(enabled) + 302(disabled)
assertThat(grant.getPermCodes()).containsExactly("crm:user:list");
}
@Test
@DisplayName("无角色用户:权限码与角色码均为空集,可见性取最窄档")
void permUnion_noRole_emptySets() {
PermissionGrant grant = resolver.resolve(9L);
assertThat(grant.getPermCodes()).isEmpty();
assertThat(grant.getRoleCodes()).isEmpty();
assertThat(grant.getVisibility()).isNotNull();
}
@Test
@DisplayName("ROLE_ 前缀归一幂等:已带前缀的 roleCode 不得双前缀")
void rolePrefix_normalization_isIdempotent() {
PermissionGrant grant = resolver.resolve(25L); // 角色5:roleCode='ROLE_ADMIN'
assertThat(grant.getRoleCodes()).containsExactly("ROLE_ADMIN");
assertThat(grant.asAuthorities())
.extracting(SimpleGrantedAuthority::getAuthority)
.contains("ROLE_ADMIN")
.doesNotContain("ROLE_ROLE_ADMIN");
}
@Test
@DisplayName("ROLE_ 前缀归一:无前缀的 roleCode 由模块补前缀")
void rolePrefix_normalization_addsWhenMissing() {
PermissionGrant grant = resolver.resolve(1L); // 角色1:roleCode='SELF'
assertThat(grant.asAuthorities())
.extracting(SimpleGrantedAuthority::getAuthority)
.contains("ROLE_SELF", "crm:user:list");
}
@Test
@DisplayName("可见菜单树:含授权的 menu 节点,排除 button,无授权返回空")
void visibleMenuTree_menusOnly() {
List<SysMenu> tree = resolver.visibleMenuTree(5L);
assertThat(tree).extracting(SysMenu::getId).contains(303L);
assertThat(tree).extracting(SysMenu::getId).doesNotContain(301L, 302L, 304L);
assertThat(resolver.visibleMenuTree(9L)).isEmpty();
}
/** 建表 + 固定数据集(BaseEntity 审计列一律允许 NULL,deleted 显式置 0) */
private static final String[] SCHEMA_AND_SEED = {
"""
@ -417,6 +483,20 @@ class DataScopeIntegrationTest {
updater_id varchar(50), update_time datetime, deleted tinyint not null default 0,
dept_id bigint, owner_id bigint, title varchar(50))
""",
"""
create table sys_menu (
id bigint primary key, creator_id varchar(50), create_time datetime,
updater_id varchar(50), update_time datetime, deleted tinyint not null default 0,
parent_id bigint not null default 0, menu_name varchar(50) not null, menu_type tinyint not null,
path varchar(200), component varchar(200), icon varchar(50), sort int default 0,
visible tinyint default 1, description varchar(100), perms varchar(100),
deny_behavior varchar(16), status varchar(16) default 'enabled', api_url varchar(200))
""",
"""
create table sys_role_menu (
id bigint primary key, role_id bigint not null, menu_id bigint not null,
constraint uk_role_menu unique (role_id, menu_id))
""",
// 部门树
"insert into sys_dept (id, deleted, parent_id, dept_name) values (10, 0, 0, '销售部')",
"insert into sys_dept (id, deleted, parent_id, dept_name) values (11, 0, 10, '华东组')",
@ -428,6 +508,8 @@ class DataScopeIntegrationTest {
"insert into sys_role (id, deleted, role_name, role_code, data_scope) values (2, 0, '本部门', 'DEPT', 2)",
"insert into sys_role (id, deleted, role_name, role_code, data_scope) values (3, 0, '含子部门', 'DEPT_CHILD', 3)",
"insert into sys_role (id, deleted, role_name, role_code, data_scope) values (4, 0, '全部', 'ALL', 4)",
// 角色5:roleCode 自带 ROLE_ 前缀(验证归一幂等,不得双前缀)
"insert into sys_role (id, deleted, role_name, role_code, data_scope) values (5, 0, '管理员', 'ROLE_ADMIN', 1)",
// 用户
"insert into crm_auth_user (id, deleted, username, dept_id) values (1, 0, '仅本人-销售部', 10)",
"insert into crm_auth_user (id, deleted, username, dept_id) values (2, 0, '本部门-销售部', 10)",
@ -441,6 +523,7 @@ class DataScopeIntegrationTest {
"insert into crm_auth_user (id, deleted, username, dept_id) values (22, 0, '兼职-主市场兼华北', 20)",
"insert into crm_auth_user (id, deleted, username, dept_id) values (23, 0, '兼职-无主部门', null)",
"insert into crm_auth_user (id, deleted, username, dept_id) values (24, 0, '兼职-层级重叠', 10)",
"insert into crm_auth_user (id, deleted, username, dept_id) values (25, 0, '带前缀角色', 10)",
// 用户-角色
"insert into sys_user_role (id, user_id, role_id) values (1001, 1, 1)",
"insert into sys_user_role (id, user_id, role_id) values (1002, 2, 2)",
@ -454,6 +537,7 @@ class DataScopeIntegrationTest {
"insert into sys_user_role (id, user_id, role_id) values (1010, 22, 3)",
"insert into sys_user_role (id, user_id, role_id) values (1011, 23, 2)",
"insert into sys_user_role (id, user_id, role_id) values (1012, 24, 3)",
"insert into sys_user_role (id, user_id, role_id) values (1013, 25, 5)",
// 兼职挂靠
"insert into sys_user_dept (id, user_id, dept_id) values (2001, 21, 20)",
"insert into sys_user_dept (id, user_id, dept_id) values (2002, 22, 12)",
@ -466,5 +550,16 @@ class DataScopeIntegrationTest {
"insert into test_owned_data (id, deleted, dept_id, owner_id, title) values (104, 0, 20, 4, '市场部-用户4')",
"insert into test_owned_data (id, deleted, dept_id, owner_id, title) values (105, 0, 20, 1, '市场部-用户1')",
"insert into test_owned_data (id, deleted, dept_id, owner_id, title) values (106, 0, 121, 8, '华北一队-用户8')",
// 权限资源树:302 是停用权限点(全局断路);303 是 menu 节点(不进权限码并集)
"insert into sys_menu (id, deleted, parent_id, menu_name, menu_type, perms, status, visible, sort) values (301, 0, 0, '用户列表按钮', 3, 'crm:user:list', 'enabled', 1, 0)",
"insert into sys_menu (id, deleted, parent_id, menu_name, menu_type, perms, status, visible, sort) values (302, 0, 0, '用户删除按钮', 3, 'crm:user:delete', 'disabled', 1, 0)",
"insert into sys_menu (id, deleted, parent_id, menu_name, menu_type, path, visible, sort) values (303, 0, 0, '订单菜单', 2, '/order', 1, 0)",
"insert into sys_menu (id, deleted, parent_id, menu_name, menu_type, perms, status, visible, sort) values (304, 0, 303, '订单列表按钮', 3, 'crm:order:list', 'enabled', 1, 0)",
// 角色授权:角色1→301,302;角色2→302,304,303
"insert into sys_role_menu (id, role_id, menu_id) values (5001, 1, 301)",
"insert into sys_role_menu (id, role_id, menu_id) values (5002, 1, 302)",
"insert into sys_role_menu (id, role_id, menu_id) values (5003, 2, 302)",
"insert into sys_role_menu (id, role_id, menu_id) values (5004, 2, 304)",
"insert into sys_role_menu (id, role_id, menu_id) values (5005, 2, 303)",
};
}

Loading…
Cancel
Save