From c877f0d6c49cbea55f41f935311da9999ac93e45 Mon Sep 17 00:00:00 2001
From: luoweijian <1329394916@qq.com>
Date: Tue, 4 Aug 2026 17:01:36 +0800
Subject: [PATCH] =?UTF-8?q?refactor(auth):=20=E6=9D=83=E9=99=90=E8=A7=A3?=
=?UTF-8?q?=E6=9E=90=E6=94=B6=E6=88=90=E6=B7=B1=E6=A8=A1=E5=9D=97=20Permis?=
=?UTF-8?q?sionResolver?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
将 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 个权限码并集场景
---
.../crm/auth/controller/SystemController.java | 6 +-
.../security/JwtAuthenticationFilter.java | 17 +--
.../crm/auth/security/PermissionGrant.java | 67 +++++++++
.../crm/auth/security/PermissionResolver.java | 35 +++++
.../auth/security/PermissionResolverImpl.java | 139 ++++++++++++++++++
.../service/impl/PermissionServiceImpl.java | 123 ----------------
.../scope/DataScopeIntegrationTest.java | 119 +++++++++++++--
7 files changed, 357 insertions(+), 149 deletions(-)
create mode 100644 crm-auth/src/main/java/com/crm/auth/security/PermissionGrant.java
create mode 100644 crm-auth/src/main/java/com/crm/auth/security/PermissionResolver.java
create mode 100644 crm-auth/src/main/java/com/crm/auth/security/PermissionResolverImpl.java
delete mode 100644 crm-auth/src/main/java/com/crm/auth/service/impl/PermissionServiceImpl.java
diff --git a/crm-auth/src/main/java/com/crm/auth/controller/SystemController.java b/crm-auth/src/main/java/com/crm/auth/controller/SystemController.java
index 0668d9a..b54d7e7 100644
--- a/crm-auth/src/main/java/com/crm/auth/controller/SystemController.java
+++ b/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 不可变。纯取值对象——装载 {@link DataVisibilityContext} 是请求生命周期所有者
+ * (JWT 过滤器)的职责,本对象不产生副作用。 深模块:调用方只见两个方法,「用户 → 角色 → 授权 → 资源」的查询链、
+ * 档位取舍、子树展开、权限码并集的断路规则全部藏在实现里。 fail-closed(ADR-0006):解析失败即抛异常,由请求入口拒绝该请求;
+ * 不存在「部分成功」的第三状态。 角色编码由 JWT 过滤器填充为 Spring Security authority,使 {@code @PreAuthorize(hasRole(...))}
- * 生效(ADR-0008);无角色返回空集。 给定「用户 + 角色档位 + 主部门 + 部门树」,经真实的 {@link PermissionServiceImpl#initDataScopeContext}
+ * 给定「用户 + 角色档位 + 主部门 + 部门树」,经真实的 {@link PermissionResolver#resolve}
* 与真实的 Mapper 查询(H2 MySQL 模式 + {@link DataScopeInterceptor}),断言测试专用受控实体
- * {@link TestOwnedData} 的可见行集合。不断言生成的 SQL 字符串。> menuTree() {
Long userId = Long.valueOf(SecurityUtils.getRequiredUserId());
- return Result.success(permissionService.getMenuTree(userId));
+ return Result.success(permissionResolver.visibleMenuTree(userId));
}
@PostMapping("/menus/save")
diff --git a/crm-auth/src/main/java/com/crm/auth/security/JwtAuthenticationFilter.java b/crm-auth/src/main/java/com/crm/auth/security/JwtAuthenticationFilter.java
index 5275557..e7e6e6e 100644
--- a/crm-auth/src/main/java/com/crm/auth/security/JwtAuthenticationFilter.java
+++ b/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
+ *
+ */
+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
固定数据集:
*
@@ -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 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 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)",
};
}