Kaynağa Gözat

1. 增加 Job 的多租户的能力

YunaiV 3 yıl önce
ebeveyn
işleme
6cd9b3bf7e
19 değiştirilmiş dosya ile 327 ekleme ve 10 silme
  1. 2 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/job/auth/SysUserSessionTimeoutJob.java
  2. 1 1
      yudao-admin-server/src/main/resources/application.yaml
  3. 2 2
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/system/dal/dataobject/auth/SysUserSessionDO.java
  4. 45 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/system/dal/dataobject/tenant/SysTenantDO.java
  5. 9 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/system/dal/mysql/tenant/SysTenantCoreMapper.java
  6. 11 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/system/service/tenant/SysTenantCoreService.java
  7. 29 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/system/service/tenant/impl/SysTenantCoreServiceImpl.java
  8. 16 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java
  9. 48 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java
  10. 2 1
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java
  11. 6 0
      yudao-framework/yudao-spring-boot-starter-tenant/pom.xml
  12. 41 0
      yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantJobAutoConfiguration.java
  13. 14 0
      yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java
  14. 58 0
      yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerDecorator.java
  15. 14 0
      yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerInvoker.java
  16. 19 0
      yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkService.java
  17. 4 1
      yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantWebFilter.java
  18. 4 4
      yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java
  19. 2 1
      yudao-framework/yudao-spring-boot-starter-tenant/src/main/resources/META-INF/spring.factories

+ 2 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/job/auth/SysUserSessionTimeoutJob.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.adminserver.modules.system.job.auth;
 
 import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
 import cn.iocoder.yudao.adminserver.modules.system.service.auth.SysUserSessionService;
+import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
@@ -13,6 +14,7 @@ import javax.annotation.Resource;
  * @author 願
  */
 @Component
+@TenantJob
 @Slf4j
 public class SysUserSessionTimeoutJob implements JobHandler {
 

+ 1 - 1
yudao-admin-server/src/main/resources/application.yaml

@@ -74,6 +74,6 @@ yudao:
       - cn.iocoder.yudao.adminserver.modules.infra.enums.InfErrorCodeConstants
       - cn.iocoder.yudao.adminserver.modules.system.enums.SysErrorCodeConstants
   tenant:
-    tables: sys_user
+    tables: sys_user, sys_user_session
 
 debug: false

+ 2 - 2
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/system/dal/dataobject/auth/SysUserSessionDO.java

@@ -1,8 +1,8 @@
 package cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.auth;
 
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
-import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
@@ -25,7 +25,7 @@ import java.util.Date;
 @Data
 @Builder
 @EqualsAndHashCode(callSuper = true)
-public class SysUserSessionDO extends BaseDO {
+public class SysUserSessionDO extends TenantBaseDO {
 
     /**
      * 会话编号, 即 sessionId

+ 45 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/system/dal/dataobject/tenant/SysTenantDO.java

@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.tenant;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 租户 DO
+ *
+ * @author 芋道源码
+ */
+@TableName(value = "sys_tenant", autoResultMap = true)
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class SysTenantDO extends BaseDO {
+
+    /**
+     * 租户编号,自增
+     */
+    private Long id;
+    /**
+     * 租户名,唯一
+     */
+    private String name;
+    /**
+     * 联系人
+     */
+    private String contactName;
+    /**
+     * 联系手机
+     */
+    private String contactMobile;
+    /**
+     * 帐号状态
+     *
+     * 枚举 {@link CommonStatusEnum}
+     */
+    private Integer status;
+
+}

+ 9 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/system/dal/mysql/tenant/SysTenantCoreMapper.java

@@ -0,0 +1,9 @@
+package cn.iocoder.yudao.coreservice.modules.system.dal.mysql.tenant;
+
+import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.tenant.SysTenantDO;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface SysTenantCoreMapper extends BaseMapperX<SysTenantDO> {
+}

+ 11 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/system/service/tenant/SysTenantCoreService.java

@@ -0,0 +1,11 @@
+package cn.iocoder.yudao.coreservice.modules.system.service.tenant;
+
+import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
+
+/**
+ * 租户 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface SysTenantCoreService extends TenantFrameworkService {
+}

+ 29 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/system/service/tenant/impl/SysTenantCoreServiceImpl.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.coreservice.modules.system.service.tenant.impl;
+
+import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.tenant.SysTenantDO;
+import cn.iocoder.yudao.coreservice.modules.system.dal.mysql.tenant.SysTenantCoreMapper;
+import cn.iocoder.yudao.coreservice.modules.system.service.tenant.SysTenantCoreService;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+/**
+ * 租户 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+public class SysTenantCoreServiceImpl implements SysTenantCoreService {
+
+    @Resource
+    private SysTenantCoreMapper tenantCoreMapper;
+
+    @Override
+    public List<Long> getTenantIds() {
+        List<SysTenantDO> tenants = tenantCoreMapper.selectList();
+        return CollectionUtils.convertList(tenants, SysTenantDO::getId);
+    }
+
+}

+ 16 - 0
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java

@@ -1,15 +1,18 @@
 package cn.iocoder.yudao.framework.security.config;
 
 import cn.iocoder.yudao.framework.security.core.aop.PreAuthenticatedAspect;
+import cn.iocoder.yudao.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy;
 import cn.iocoder.yudao.framework.security.core.filter.JWTAuthenticationTokenFilter;
 import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl;
 import cn.iocoder.yudao.framework.security.core.handler.AuthenticationEntryPointImpl;
 import cn.iocoder.yudao.framework.security.core.handler.LogoutSuccessHandlerImpl;
 import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService;
 import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
+import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.web.AuthenticationEntryPoint;
@@ -85,4 +88,17 @@ public class YudaoSecurityAutoConfiguration {
         return new JWTAuthenticationTokenFilter(securityProperties, securityFrameworkService, globalExceptionHandler);
     }
 
+    /**
+     * 声明调用 {@link SecurityContextHolder#setStrategyName(String)} 方法,
+     * 设置使用 {@link TransmittableThreadLocalSecurityContextHolderStrategy} 作为 Security 的上下文策略
+     */
+    @Bean
+    public MethodInvokingFactoryBean securityContextHolderMethodInvokingFactoryBean() {
+        MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean();
+        methodInvokingFactoryBean.setTargetClass(SecurityContextHolder.class);
+        methodInvokingFactoryBean.setTargetMethod("setStrategyName");
+        methodInvokingFactoryBean.setArguments(TransmittableThreadLocalSecurityContextHolderStrategy.class.getName());
+        return methodInvokingFactoryBean;
+    }
+
 }

+ 48 - 0
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java

@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.framework.security.core.context;
+
+import com.alibaba.ttl.TransmittableThreadLocal;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolderStrategy;
+import org.springframework.security.core.context.SecurityContextImpl;
+import org.springframework.util.Assert;
+
+/**
+ * 基于 TransmittableThreadLocal 实现的 Security Context 持有者策略
+ * 目的是,避免 @Async 等异步执行时,原生 ThreadLocal 的丢失问题
+ *
+ * @author 芋道源码
+ */
+public class TransmittableThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
+
+    /**
+     * 使用 TransmittableThreadLocal 作为上下文
+     */
+    private static final ThreadLocal<SecurityContext> contextHolder = new TransmittableThreadLocal<>();
+
+    @Override
+    public void clearContext() {
+        contextHolder.remove();
+    }
+
+    @Override
+    public SecurityContext getContext() {
+        SecurityContext ctx = contextHolder.get();
+        if (ctx == null) {
+            ctx = createEmptyContext();
+            contextHolder.set(ctx);
+        }
+        return ctx;
+    }
+
+    @Override
+    public void setContext(SecurityContext context) {
+        Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
+        contextHolder.set(context);
+    }
+
+    @Override
+    public SecurityContext createEmptyContext() {
+        return new SecurityContextImpl();
+    }
+
+}

+ 2 - 1
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java

@@ -95,13 +95,14 @@ public class SecurityFrameworkUtils {
                 loginUser, null, loginUser.getAuthorities());
         authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
         // 设置到上下文
-        //何时调用  SecurityContextHolder.clearContext. spring security filter 应该会调用 clearContext
         SecurityContextHolder.getContext().setAuthentication(authenticationToken);
         // 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号;
         // 原因是,Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息
         WebFrameworkUtils.setLoginUserId(request, loginUser.getId());
         WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType());
         // TODO @jason:使用 userId 会不会更合适哈?
+        // TODO @芋艿:activiti 需要使用 ttl 上下文
+        // TODO @jason:清理问题
         if (Objects.equals(UserTypeEnum.ADMIN.getValue(), loginUser.getUserType())) {
             org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(loginUser.getUsername());
         }

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

@@ -32,6 +32,12 @@
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
         </dependency>
+
+        <!-- Job 定时任务相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-job</artifactId>
+        </dependency>
     </dependencies>
 
 </project>

+ 41 - 0
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantJobAutoConfiguration.java

@@ -0,0 +1,41 @@
+package cn.iocoder.yudao.framework.tenant.config;
+
+import cn.hutool.core.annotation.AnnotationUtil;
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
+import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
+import cn.iocoder.yudao.framework.tenant.core.job.TenantJobHandlerDecorator;
+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;
+
+/**
+ * 多租户针对 Job 的自动配置
+ *
+ * @author 芋道源码
+ */
+public class YudaoTenantJobAutoConfiguration {
+
+    @Bean
+    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
+    public BeanPostProcessor jobHandlerBeanPostProcessor(TenantFrameworkService tenantFrameworkService) {
+        return new BeanPostProcessor() {
+
+            @Override
+            public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
+                if (!(bean instanceof JobHandler)) {
+                    return bean;
+                }
+                // 有 TenantJob 注解的情况下,才会进行处理
+                if (!AnnotationUtil.hasAnnotation(bean.getClass(), TenantJob.class)) {
+                    return bean;
+                }
+
+                // 使用 TenantJobHandlerDecorator 装饰
+                return new TenantJobHandlerDecorator(tenantFrameworkService, (JobHandler) bean);
+            }
+
+        };
+    }
+
+}

+ 14 - 0
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.framework.tenant.core.job;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 多租户 Job 注解
+ */
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface TenantJob {
+}

+ 58 - 0
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerDecorator.java

@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.framework.tenant.core.job;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
+import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
+import lombok.AllArgsConstructor;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 多租户 JobHandler 装饰器
+ * 任务执行时,会按照租户逐个执行 Job 的逻辑
+ *
+ * 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。
+ *
+ * @author 芋道源码
+ */
+@AllArgsConstructor
+public class TenantJobHandlerDecorator implements JobHandler {
+
+    private final TenantFrameworkService tenantFrameworkService;
+    /**
+     * 被装饰的 Job
+     */
+    private final JobHandler jobHandler;
+
+    @Override
+    public final String execute(String param) throws Exception {
+        // 获得租户列表
+        List<Long> tenantIds = tenantFrameworkService.getTenantIds();
+        if (CollUtil.isEmpty(tenantIds)) {
+            return null;
+        }
+
+        // 逐个租户,执行 Job
+        Map<Long, String> results = new ConcurrentHashMap<>();
+        tenantIds.parallelStream().forEach(tenantId -> { // TODO 芋艿:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况
+            try {
+                // 设置租户
+                TenantContextHolder.setTenantId(tenantId);
+                // 执行 Job
+                String result = jobHandler.execute(param);
+                // 添加结果
+                results.put(tenantId, result);
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            } finally {
+                TenantContextHolder.clear();
+            }
+        });
+        return JsonUtils.toJsonString(results);
+    }
+
+}

+ 14 - 0
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerInvoker.java

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.framework.tenant.core.job;
+
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandlerInvoker;
+
+/**
+ * 多租户 JobHandlerInvoker 拓展实现类
+ *
+ * @author 芋道源码
+ */
+public class TenantJobHandlerInvoker extends JobHandlerInvoker {
+
+
+
+}

+ 19 - 0
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkService.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.framework.tenant.core.service;
+
+import java.util.List;
+
+/**
+ * Tenant 框架 Service 接口,定义获取租户信息
+ *
+ * @author 芋道源码
+ */
+public interface TenantFrameworkService {
+
+    /**
+     * 获得所有租户
+     *
+     * @return 租户编号数组
+     */
+    List<Long> getTenantIds();
+
+}

+ 4 - 1
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantWebFilter.java

@@ -12,7 +12,10 @@ import java.io.IOException;
 
 /**
  * 多租户 Web 过滤器
- * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号
+ * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。
+ *
+ * Q:会不会存在模拟 tenant-id 导致跨租户的问题?
+ * A:用户登陆后,获得的 Token 是基于租户级别隔离,从而保证授权失败。
  *
  * @author 芋道源码
  */

+ 4 - 4
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java

@@ -1,8 +1,8 @@
 /**
  * 多租户,支持如下层面:
- * 1. DB:基于 MyBatis Plus 多租户的功能实现
- * 2. Job:TODO
- * 3. MQ:TODO
- * 4. Web:TODO
+ * 1. DB:基于 MyBatis Plus 多租户的功能实现
+ * 2. Web:请求 HTTP API 时,Header 带上 tenant-id 租户编号。
+ * 3. Job:在 JobHandler 执行任务时,会按照每个租户,都独立并行执行一次。
+ * 4. MQ:TODO
  */
 package cn.iocoder.yudao.framework.tenant;

+ 2 - 1
yudao-framework/yudao-spring-boot-starter-tenant/src/main/resources/META-INF/spring.factories

@@ -1,3 +1,4 @@
 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.YudaoTenantWebAutoConfiguration,\
+  cn.iocoder.yudao.framework.tenant.config.YudaoTenantJobAutoConfiguration