Przeglądaj źródła

增加 Tenant Security 的实现

YunaiV 3 lat temu
rodzic
commit
8795a4cdeb
13 zmienionych plików z 129 dodań i 14 usunięć
  1. 1 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/controller/auth/SysAuthController.http
  2. 6 4
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java
  3. 5 0
      yudao-framework/yudao-spring-boot-starter-tenant/pom.xml
  4. 9 0
      yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/TenantProperties.java
  5. 2 0
      yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantJobAutoConfiguration.java
  6. 25 0
      yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantSecurityAutoConfiguration.java
  7. 2 0
      yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java
  8. 18 0
      yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java
  9. 1 2
      yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/db/TenantDatabaseInterceptor.java
  10. 1 1
      yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java
  11. 50 0
      yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/security/TenantSecurityWebFilter.java
  12. 7 5
      yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java
  13. 2 1
      yudao-framework/yudao-spring-boot-starter-tenant/src/main/resources/META-INF/spring.factories

+ 1 - 1
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 临时测试

+ 6 - 4
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;
 

+ 5 - 0
yudao-framework/yudao-spring-boot-starter-tenant/pom.xml

@@ -27,6 +27,11 @@
             <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-security</artifactId>
+        </dependency>
+
         <!-- DB 相关 -->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>

+ 9 - 0
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;
     /**
      * 需要多租户的表
      *

+ 2 - 0
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

+ 25 - 0
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> tenantSecurityWebFilter() {
+        FilterRegistrationBean<TenantSecurityWebFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(new TenantSecurityWebFilter());
+        registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER);
+        return registrationBean;
+    }
+
+}

+ 2 - 0
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

+ 18 - 0
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<Long> 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);
     }

+ 1 - 2
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

+ 1 - 1
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);
     }
 

+ 50 - 0
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;
+    }
+
+}

+ 7 - 5
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;

+ 2 - 1
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