diff --git a/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/SysAuthController.http b/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/SysAuthController.http index c98984b69..f8e32a5b8 100644 --- a/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/SysAuthController.http +++ b/yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/SysAuthController.http @@ -18,7 +18,7 @@ tenant-id: 1 ### 请求 /list-menus 接口 => 成功 GET {{baseUrl}}/list-menus Authorization: Bearer {{token}} -#Authorization: Bearer 0d161f69c9ac4c7f836e1b850715a7b0 +#Authorization: Bearer a6aa7714a2e44c95aaa8a2c5adc2a67a tenant-id: 1 ### 请求 /druid/xxx 接口 => 失败 TODO 临时测试 diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java index 9a8acca80..2c4dfc4c6 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java @@ -17,13 +17,15 @@ public interface WebFilterOrderEnum { // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等 - int TENANT_CONTEXT_FILTER = - 100; // 需要保证在 ApiAccessLogFilter 前面 + int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面 - int API_ACCESS_LOG_FILTER = -90; // 需要保证在 RequestBodyCacheFilter 后面 + int API_ACCESS_LOG_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面 - int XSS_FILTER = -80; // 需要保证在 RequestBodyCacheFilter 后面 + int XSS_FILTER = -102; // 需要保证在 RequestBodyCacheFilter 后面 - // Spring Security Filter 默认为 -100,可见 SecurityProperties 配置属性类 + // Spring Security Filter 默认为 -100,可见 org.springframework.boot.autoconfigure.security.SecurityProperties 配置属性类 + + int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器后 int DEMO_FILTER = Integer.MAX_VALUE; diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/pom.xml b/yudao-framework/yudao-spring-boot-starter-tenant/pom.xml index bc4302809..e7d81f3d6 100644 --- a/yudao-framework/yudao-spring-boot-starter-tenant/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-tenant/pom.xml @@ -27,6 +27,11 @@ spring-boot-starter-web + + cn.iocoder.boot + yudao-spring-boot-starter-security + + cn.iocoder.boot diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java index 5b97b22f6..33390f631 100644 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java +++ b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java @@ -14,6 +14,15 @@ import java.util.Set; @Data public class TenantProperties { +// /** +// * 租户是否开启 +// */ +// private static final Boolean ENABLE_DEFAULT = true; +// +// /** +// * 是否开启 +// */ +// private Boolean enable = ENABLE_DEFAULT; /** * 需要多租户的表 * diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantJobAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantJobAutoConfiguration.java index d9d2f3823..89b58f86e 100644 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantJobAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantJobAutoConfiguration.java @@ -8,12 +8,14 @@ import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; /** * 多租户针对 Job 的自动配置 * * @author 芋道源码 */ +@Configuration public class YudaoTenantJobAutoConfiguration { @Bean diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantSecurityAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantSecurityAutoConfiguration.java new file mode 100644 index 000000000..b5dbd0000 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantSecurityAutoConfiguration.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.framework.tenant.config; + +import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; +import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 多租户针对 Web 的自动配置 + * + * @author 芋道源码 + */ +@Configuration +public class YudaoTenantSecurityAutoConfiguration { + + @Bean + public FilterRegistrationBean tenantSecurityWebFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new TenantSecurityWebFilter()); + registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER); + return registrationBean; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java index faacbc486..c2830a79a 100644 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java @@ -4,12 +4,14 @@ import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; /** * 多租户针对 Web 的自动配置 * * @author 芋道源码 */ +@Configuration public class YudaoTenantWebAutoConfiguration { @Bean diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java index 92dbff002..cf5a94141 100644 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java +++ b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java @@ -11,10 +11,28 @@ public class TenantContextHolder { private static final ThreadLocal TENANT_ID = new TransmittableThreadLocal<>(); + /** + * 获得租户编号。 + * + * @return 租户编号 + */ public static Long getTenantId() { return TENANT_ID.get(); } + /** + * 获得租户编号。如果不存在,则抛出 NullPointerException 异常 + * + * @return 租户编号 + */ + public static Long getRequiredTenantId() { + Long tenantId = getTenantId(); + if (tenantId == null) { + throw new NullPointerException("TenantContextHolder 不存在租户编号"); + } + return tenantId; + } + public static void setTenantId(Long tenantId) { TENANT_ID.set(tenantId); } diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java index dfcd8a4a4..055a76a1e 100644 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java +++ b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java @@ -20,8 +20,7 @@ public class TenantDatabaseInterceptor implements TenantLineHandler { @Override public Expression getTenantId() { - // TODO 芋艿:暂时不考虑获取不到的情况。此时,会存在 NPE 的报错 - return new StringValue(TenantContextHolder.getTenantId().toString()); + return new StringValue(TenantContextHolder.getRequiredTenantId().toString()); } @Override diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java index f90ac1246..c23bc712e 100644 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java +++ b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java @@ -40,7 +40,7 @@ public class TenantRedisKeyDefine extends RedisKeyDefine { @Override public String formatKey(Object... args) { - args = ArrayUtil.append(args, TenantContextHolder.getTenantId()); + args = ArrayUtil.append(args, TenantContextHolder.getRequiredTenantId()); return super.formatKey(args); } diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java new file mode 100644 index 000000000..0801427da --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.framework.tenant.core.security; + +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Objects; + +/** + * 多租户 Security Web 过滤器 + * 校验用户访问的租户,是否是其所在的租户,避免越权问题 + * + * @author 芋道源码 + */ +@Slf4j +public class TenantSecurityWebFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + LoginUser user = SecurityFrameworkUtils.getLoginUser(); + assert user != null; // shouldNotFilter 已经校验 + if (!Objects.equals(user.getTenantId(), TenantContextHolder.getTenantId())) { + log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]", + user.getTenantId(), user.getId(), user.getUserType(), + TenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod()); + ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(), + "您无权访问该租户的数据")); + return; + } + // 继续过滤 + chain.doFilter(request, response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + return SecurityFrameworkUtils.getLoginUser() == null; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java index 90420e01c..aa22cdb95 100644 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java +++ b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java @@ -1,15 +1,17 @@ /** * 多租户,支持如下层面: * 1. DB:基于 MyBatis Plus 多租户的功能实现。 - * 2. Web:请求 HTTP API 时,解析 Header 的 tenant-id 租户编号,添加到租户上下文。 - * 3. Job:在 JobHandler 执行任务时,会按照每个租户,都独立并行执行一次。 - * 4. MQ:在 Producer 发送消息时,Header 带上 tenant-id 租户编号;在 Consumer 消费消息时,将 Header 的 tenant-id 租户编号,添加到租户上下文。 - * 5. Async:异步需要保证 ThreadLocal 的传递性,通过使用阿里开源的 TransmittableThreadLocal 实现。相关的改造点,可见: + * 2. Redis:通过在 Redis Key 上拼接租户编号的方式,进行隔离。 + * 3. Web:请求 HTTP API 时,解析 Header 的 tenant-id 租户编号,添加到租户上下文。 + * 4. Security:校验当前登陆的用户,是否越权访问其它租户的数据。 + * 5. Job:在 JobHandler 执行任务时,会按照每个租户,都独立并行执行一次。 + * 6. MQ:在 Producer 发送消息时,Header 带上 tenant-id 租户编号;在 Consumer 消费消息时,将 Header 的 tenant-id 租户编号,添加到租户上下文。 + * 7. Async:异步需要保证 ThreadLocal 的传递性,通过使用阿里开源的 TransmittableThreadLocal 实现。相关的改造点,可见: * 1)Spring Async: * {@link cn.iocoder.yudao.framework.quartz.config.YudaoAsyncAutoConfiguration#threadPoolTaskExecutorBeanPostProcessor()} * 2)Spring Security: * TransmittableThreadLocalSecurityContextHolderStrategy * 和 YudaoSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法 - * 6. Redis:通过在 Redis Key 上拼接租户编号的方式,进行隔离。 + * */ package cn.iocoder.yudao.framework.tenant; diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/resources/META-INF/spring.factories index 5340df3a8..159dfcfee 100644 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/resources/META-INF/spring.factories +++ b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/resources/META-INF/spring.factories @@ -2,4 +2,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ cn.iocoder.yudao.framework.tenant.config.YudaoTenantDatabaseAutoConfiguration,\ cn.iocoder.yudao.framework.tenant.config.YudaoTenantWebAutoConfiguration,\ cn.iocoder.yudao.framework.tenant.config.YudaoTenantJobAutoConfiguration,\ - cn.iocoder.yudao.framework.tenant.config.YudaoTenantMQAutoConfiguration + cn.iocoder.yudao.framework.tenant.config.YudaoTenantMQAutoConfiguration,\ + cn.iocoder.yudao.framework.tenant.config.YudaoTenantSecurityAutoConfiguration