From 59234e1eeade300a68adc8183d58f616c14e90f1 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 5 May 2025 17:27:40 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E5=A2=9E=E5=8A=A0=E7=A7=9F?= =?UTF-8?q?=E6=88=B7=E5=88=87=E6=8D=A2=E7=9A=84=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/db/DataPermissionRuleHandler.java | 7 ++ .../core/util/DataPermissionUtils.java | 22 +++++-- .../tenant/config/TenantProperties.java | 7 ++ .../config/YudaoTenantAutoConfiguration.java | 23 +++++++ .../web/TenantVisitContextInterceptor.java | 65 +++++++++++++++++++ .../framework/security/core/LoginUser.java | 4 ++ .../service/SecurityFrameworkServiceImpl.java | 19 ++++++ .../core/util/SecurityFrameworkUtils.java | 18 +++++ .../web/core/util/WebFrameworkUtils.java | 20 ++++-- .../controller/admin/user/UserController.http | 6 ++ .../src/main/resources/application.yaml | 3 + 11 files changed, 183 insertions(+), 11 deletions(-) create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantVisitContextInterceptor.java diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionRuleHandler.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionRuleHandler.java index a2778734be..477bfd99d3 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionRuleHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/db/DataPermissionRuleHandler.java @@ -12,6 +12,8 @@ import net.sf.jsqlparser.schema.Table; import java.util.List; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.skipPermissionCheck; + /** * 基于 {@link DataPermissionRule} 的数据权限处理器 * @@ -27,6 +29,11 @@ public class DataPermissionRuleHandler implements MultiDataPermissionHandler { @Override public Expression getSqlSegment(Table table, Expression where, String mappedStatementId) { + // 特殊:跨租户访问 + if (skipPermissionCheck()) { + return null; + } + // 获得 Mapper 对应的数据权限的规则 List rules = ruleFactory.getDataPermissionRule(mappedStatementId); if (CollUtil.isEmpty(rules)) { diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/util/DataPermissionUtils.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/util/DataPermissionUtils.java index 583c482b23..eb7b22371f 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/util/DataPermissionUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/util/DataPermissionUtils.java @@ -32,13 +32,12 @@ public class DataPermissionUtils { * @param runnable 逻辑 */ public static void executeIgnore(Runnable runnable) { - DataPermission dataPermission = getDisableDataPermissionDisable(); - DataPermissionContextHolder.add(dataPermission); + addDisableDataPermission(); try { // 执行 runnable runnable.run(); } finally { - DataPermissionContextHolder.remove(); + removeDataPermission(); } } @@ -50,14 +49,25 @@ public class DataPermissionUtils { */ @SneakyThrows public static T executeIgnore(Callable callable) { - DataPermission dataPermission = getDisableDataPermissionDisable(); - DataPermissionContextHolder.add(dataPermission); + addDisableDataPermission(); try { // 执行 callable return callable.call(); } finally { - DataPermissionContextHolder.remove(); + removeDataPermission(); } } + /** + * 添加忽略数据权限 + */ + public static void addDisableDataPermission(){ + DataPermission dataPermission = getDisableDataPermissionDisable(); + DataPermissionContextHolder.add(dataPermission); + } + + public static void removeDataPermission(){ + DataPermissionContextHolder.remove(); + } + } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java index 06c67694e6..e059e28090 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java @@ -33,6 +33,13 @@ public class TenantProperties { */ private Set ignoreUrls = new HashSet<>(); + /** + * 需要忽略跨(切换)租户访问的请求 + * + * 原因是:某些接口,访问的是个人信息,在跨租户是获取不到的! + */ + private Set ignoreVisitUrls = Collections.emptySet(); + /** * 需要忽略多租户的表 * diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java index c142e3dbb4..2027da77f6 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.tenant.config; import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; import cn.iocoder.yudao.framework.redis.config.YudaoCacheProperties; +import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnoreAspect; import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor; @@ -15,6 +16,7 @@ import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter; import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService; import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkServiceImpl; import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter; +import cn.iocoder.yudao.framework.tenant.core.web.TenantVisitContextInterceptor; import cn.iocoder.yudao.framework.web.config.WebProperties; import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; import cn.iocoder.yudao.module.system.api.tenant.TenantApi; @@ -36,6 +38,8 @@ import org.springframework.data.redis.cache.RedisCacheWriter; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.util.pattern.PathPattern; @@ -115,6 +119,25 @@ public class YudaoTenantAutoConfiguration { } } + @Bean + public TenantVisitContextInterceptor tenantVisitContextInterceptor(TenantProperties tenantProperties, + SecurityFrameworkService securityFrameworkService) { + return new TenantVisitContextInterceptor(tenantProperties, securityFrameworkService); + } + + @Bean + public WebMvcConfigurer tenantWebMvcConfigurer(TenantProperties tenantProperties, + TenantVisitContextInterceptor tenantVisitContextInterceptor) { + return new WebMvcConfigurer() { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(tenantVisitContextInterceptor) + .excludePathPatterns(tenantProperties.getIgnoreVisitUrls().toArray(new String[0])); + } + }; + } + // ========== Security ========== @Bean diff --git a/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantVisitContextInterceptor.java b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantVisitContextInterceptor.java new file mode 100644 index 0000000000..9ac70d8313 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantVisitContextInterceptor.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.framework.tenant.core.web; + +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.framework.tenant.config.TenantProperties; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.servlet.HandlerInterceptor; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0; + +@RequiredArgsConstructor +@Slf4j +public class TenantVisitContextInterceptor implements HandlerInterceptor { + + private static final String PERMISSION = "system:tenant:visit"; + + private final TenantProperties tenantProperties; + + private final SecurityFrameworkService securityFrameworkService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + // 如果和当前租户编号一致,则直接跳过 + Long visitTenantId = WebFrameworkUtils.getVisitTenantId(request); + if (visitTenantId == null) { + return true; + } + if (ObjUtil.equal(visitTenantId, TenantContextHolder.getTenantId())) { + return true; + } + // 必须是登录用户 + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + if (loginUser == null) { + return true; + } + + // 校验用户是否可切换租户 + if (!securityFrameworkService.hasAnyPermissions(PERMISSION)) { + throw exception0(GlobalErrorCodeConstants.FORBIDDEN.getCode(), "您无权切换租户"); + } + + // 【重点】切换租户编号 + loginUser.setVisitTenantId(visitTenantId); + TenantContextHolder.setTenantId(visitTenantId); + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + // 【重点】清理切换,换回原租户编号 + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + if (loginUser != null && loginUser.getTenantId() != null) { + TenantContextHolder.setTenantId(loginUser.getTenantId()); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java index e23c3a7a2f..a2cd9e50b6 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java @@ -56,6 +56,10 @@ public class LoginUser { */ @JsonIgnore private Map context; + /** + * 访问的租户编号 + */ + private Long visitTenantId; public void setContext(String key, Object value) { if (context == null) { diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java index b04b072215..b72ab382da 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/service/SecurityFrameworkServiceImpl.java @@ -9,6 +9,7 @@ import lombok.AllArgsConstructor; import java.util.Arrays; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.skipPermissionCheck; /** * 默认的 {@link SecurityFrameworkService} 实现类 @@ -27,6 +28,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService { @Override public boolean hasAnyPermissions(String... permissions) { + // 特殊:跨租户访问 + if (skipPermissionCheck()) { + return true; + } + + // 权限校验 Long userId = getLoginUserId(); if (userId == null) { return false; @@ -41,6 +48,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService { @Override public boolean hasAnyRoles(String... roles) { + // 特殊:跨租户访问 + if (skipPermissionCheck()) { + return true; + } + + // 权限校验 Long userId = getLoginUserId(); if (userId == null) { return false; @@ -55,6 +68,12 @@ public class SecurityFrameworkServiceImpl implements SecurityFrameworkService { @Override public boolean hasAnyScopes(String... scope) { + // 特殊:跨租户访问 + if (skipPermissionCheck()) { + return true; + } + + // 权限校验 LoginUser user = SecurityFrameworkUtils.getLoginUser(); if (user == null) { return false; diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java index 14c089a1cb..98dc7afb8d 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.framework.security.core.util; import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.security.core.LoginUser; import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; @@ -137,4 +138,21 @@ public class SecurityFrameworkUtils { return authenticationToken; } + /** + * 是否条件跳过权限校验,包括数据权限、功能权限 + * + * @return 是否跳过 + */ + public static boolean skipPermissionCheck() { + LoginUser loginUser = getLoginUser(); + if (loginUser == null) { + return false; + } + if (loginUser.getVisitTenantId() == null) { + return false; + } + // 重点:跨租户访问时,无法进行权限校验 + return ObjUtil.notEqual(loginUser.getVisitTenantId(), loginUser.getTenantId()); + } + } diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java index ae18634c29..b4aa301676 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java @@ -1,19 +1,16 @@ package cn.iocoder.yudao.framework.web.core.util; import cn.hutool.core.util.NumberUtil; -import cn.hutool.extra.servlet.ServletUtil; import cn.iocoder.yudao.framework.common.enums.TerminalEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.web.config.WebProperties; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.http.HttpServletRequest; - /** * 专属于 web 包的工具类 * @@ -27,6 +24,7 @@ public class WebFrameworkUtils { private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result"; public static final String HEADER_TENANT_ID = "tenant-id"; + public static final String HEADER_VISIT_TENANT_ID = "visit-tenant-id"; /** * 终端的 Header @@ -53,6 +51,18 @@ public class WebFrameworkUtils { return NumberUtil.isNumber(tenantId) ? Long.valueOf(tenantId) : null; } + /** + * 获得访问的租户编号,从 header 中 + * 考虑到其它 framework 组件也会使用到租户编号,所以不得不放在 WebFrameworkUtils 统一提供 + * + * @param request 请求 + * @return 租户编号 + */ + public static Long getVisitTenantId(HttpServletRequest request) { + String tenantId = request.getHeader(HEADER_VISIT_TENANT_ID); + return NumberUtil.isNumber(tenantId)? Long.valueOf(tenantId) : null; + } + public static void setLoginUserId(ServletRequest request, Long userId) { request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId); } diff --git a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.http b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.http index 90f0c98324..0ca69150aa 100644 --- a/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.http +++ b/yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.http @@ -3,3 +3,9 @@ GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10 Authorization: Bearer {{token}} #Authorization: Bearer test100 tenant-id: {{adminTenantId}} + +### 请求 /system/user/page 接口(测试访问别的租户) +GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10 +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} +visit-tenant-id: 122 \ No newline at end of file diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index f4da9e1389..14a6f9faa5 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -274,6 +274,9 @@ yudao: enable: true ignore-urls: - /jmreport/* # 积木报表,无法携带租户编号 + ignore-visit-urls: + - /admin-api/system/user/profile/** + - /admin-api/system/auth/** ignore-tables: ignore-caches: - user_role_ids