Commit 6cd9b3bf7e2432660b5e434bfb30ac1d64238b9f

Authored by YunaiV
1 parent 535d3c9c

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

Showing 19 changed files with 327 additions and 10 deletions
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;
2 2  
3 3 import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
4 4 import cn.iocoder.yudao.adminserver.modules.system.service.auth.SysUserSessionService;
  5 +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
5 6 import lombok.extern.slf4j.Slf4j;
6 7 import org.springframework.stereotype.Component;
7 8  
... ... @@ -13,6 +14,7 @@ import javax.annotation.Resource;
13 14 * @author 願
14 15 */
15 16 @Component
  17 +@TenantJob
16 18 @Slf4j
17 19 public class SysUserSessionTimeoutJob implements JobHandler {
18 20  
... ...
yudao-admin-server/src/main/resources/application.yaml
... ... @@ -74,6 +74,6 @@ yudao:
74 74 - cn.iocoder.yudao.adminserver.modules.infra.enums.InfErrorCodeConstants
75 75 - cn.iocoder.yudao.adminserver.modules.system.enums.SysErrorCodeConstants
76 76 tenant:
77   - tables: sys_user
  77 + tables: sys_user, sys_user_session
78 78  
79 79 debug: false
... ...
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/system/dal/dataobject/auth/SysUserSessionDO.java
1 1 package cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.auth;
2 2  
3 3 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
4   -import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
5 4 import cn.iocoder.yudao.framework.security.core.LoginUser;
  5 +import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
6 6 import com.baomidou.mybatisplus.annotation.IdType;
7 7 import com.baomidou.mybatisplus.annotation.TableId;
8 8 import com.baomidou.mybatisplus.annotation.TableName;
... ... @@ -25,7 +25,7 @@ import java.util.Date;
25 25 @Data
26 26 @Builder
27 27 @EqualsAndHashCode(callSuper = true)
28   -public class SysUserSessionDO extends BaseDO {
  28 +public class SysUserSessionDO extends TenantBaseDO {
29 29  
30 30 /**
31 31 * 会话编号, 即 sessionId
... ...
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/system/dal/dataobject/tenant/SysTenantDO.java 0 → 100644
  1 +package cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.tenant;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  5 +import com.baomidou.mybatisplus.annotation.TableName;
  6 +import lombok.*;
  7 +
  8 +/**
  9 + * 租户 DO
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +@TableName(value = "sys_tenant", autoResultMap = true)
  14 +@Data
  15 +@EqualsAndHashCode(callSuper = true)
  16 +@ToString(callSuper = true)
  17 +@Builder
  18 +@AllArgsConstructor
  19 +@NoArgsConstructor
  20 +public class SysTenantDO extends BaseDO {
  21 +
  22 + /**
  23 + * 租户编号,自增
  24 + */
  25 + private Long id;
  26 + /**
  27 + * 租户名,唯一
  28 + */
  29 + private String name;
  30 + /**
  31 + * 联系人
  32 + */
  33 + private String contactName;
  34 + /**
  35 + * 联系手机
  36 + */
  37 + private String contactMobile;
  38 + /**
  39 + * 帐号状态
  40 + *
  41 + * 枚举 {@link CommonStatusEnum}
  42 + */
  43 + private Integer status;
  44 +
  45 +}
... ...
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/system/dal/mysql/tenant/SysTenantCoreMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.coreservice.modules.system.dal.mysql.tenant;
  2 +
  3 +import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.tenant.SysTenantDO;
  4 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface SysTenantCoreMapper extends BaseMapperX<SysTenantDO> {
  9 +}
... ...
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/system/service/tenant/SysTenantCoreService.java 0 → 100644
  1 +package cn.iocoder.yudao.coreservice.modules.system.service.tenant;
  2 +
  3 +import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
  4 +
  5 +/**
  6 + * 租户 Service 接口
  7 + *
  8 + * @author 芋道源码
  9 + */
  10 +public interface SysTenantCoreService extends TenantFrameworkService {
  11 +}
... ...
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/system/service/tenant/impl/SysTenantCoreServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.coreservice.modules.system.service.tenant.impl;
  2 +
  3 +import cn.iocoder.yudao.coreservice.modules.system.dal.dataobject.tenant.SysTenantDO;
  4 +import cn.iocoder.yudao.coreservice.modules.system.dal.mysql.tenant.SysTenantCoreMapper;
  5 +import cn.iocoder.yudao.coreservice.modules.system.service.tenant.SysTenantCoreService;
  6 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
  7 +import org.springframework.stereotype.Service;
  8 +
  9 +import javax.annotation.Resource;
  10 +import java.util.List;
  11 +
  12 +/**
  13 + * 租户 Service 实现类
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Service
  18 +public class SysTenantCoreServiceImpl implements SysTenantCoreService {
  19 +
  20 + @Resource
  21 + private SysTenantCoreMapper tenantCoreMapper;
  22 +
  23 + @Override
  24 + public List<Long> getTenantIds() {
  25 + List<SysTenantDO> tenants = tenantCoreMapper.selectList();
  26 + return CollectionUtils.convertList(tenants, SysTenantDO::getId);
  27 + }
  28 +
  29 +}
... ...
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java
1 1 package cn.iocoder.yudao.framework.security.config;
2 2  
3 3 import cn.iocoder.yudao.framework.security.core.aop.PreAuthenticatedAspect;
  4 +import cn.iocoder.yudao.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy;
4 5 import cn.iocoder.yudao.framework.security.core.filter.JWTAuthenticationTokenFilter;
5 6 import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl;
6 7 import cn.iocoder.yudao.framework.security.core.handler.AuthenticationEntryPointImpl;
7 8 import cn.iocoder.yudao.framework.security.core.handler.LogoutSuccessHandlerImpl;
8 9 import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService;
9 10 import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
  11 +import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
10 12 import org.springframework.boot.context.properties.EnableConfigurationProperties;
11 13 import org.springframework.context.annotation.Bean;
12 14 import org.springframework.context.annotation.Configuration;
  15 +import org.springframework.security.core.context.SecurityContextHolder;
13 16 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
14 17 import org.springframework.security.crypto.password.PasswordEncoder;
15 18 import org.springframework.security.web.AuthenticationEntryPoint;
... ... @@ -85,4 +88,17 @@ public class YudaoSecurityAutoConfiguration {
85 88 return new JWTAuthenticationTokenFilter(securityProperties, securityFrameworkService, globalExceptionHandler);
86 89 }
87 90  
  91 + /**
  92 + * 声明调用 {@link SecurityContextHolder#setStrategyName(String)} 方法,
  93 + * 设置使用 {@link TransmittableThreadLocalSecurityContextHolderStrategy} 作为 Security 的上下文策略
  94 + */
  95 + @Bean
  96 + public MethodInvokingFactoryBean securityContextHolderMethodInvokingFactoryBean() {
  97 + MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean();
  98 + methodInvokingFactoryBean.setTargetClass(SecurityContextHolder.class);
  99 + methodInvokingFactoryBean.setTargetMethod("setStrategyName");
  100 + methodInvokingFactoryBean.setArguments(TransmittableThreadLocalSecurityContextHolderStrategy.class.getName());
  101 + return methodInvokingFactoryBean;
  102 + }
  103 +
88 104 }
... ...
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.security.core.context;
  2 +
  3 +import com.alibaba.ttl.TransmittableThreadLocal;
  4 +import org.springframework.security.core.context.SecurityContext;
  5 +import org.springframework.security.core.context.SecurityContextHolderStrategy;
  6 +import org.springframework.security.core.context.SecurityContextImpl;
  7 +import org.springframework.util.Assert;
  8 +
  9 +/**
  10 + * 基于 TransmittableThreadLocal 实现的 Security Context 持有者策略
  11 + * 目的是,避免 @Async 等异步执行时,原生 ThreadLocal 的丢失问题
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +public class TransmittableThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
  16 +
  17 + /**
  18 + * 使用 TransmittableThreadLocal 作为上下文
  19 + */
  20 + private static final ThreadLocal<SecurityContext> contextHolder = new TransmittableThreadLocal<>();
  21 +
  22 + @Override
  23 + public void clearContext() {
  24 + contextHolder.remove();
  25 + }
  26 +
  27 + @Override
  28 + public SecurityContext getContext() {
  29 + SecurityContext ctx = contextHolder.get();
  30 + if (ctx == null) {
  31 + ctx = createEmptyContext();
  32 + contextHolder.set(ctx);
  33 + }
  34 + return ctx;
  35 + }
  36 +
  37 + @Override
  38 + public void setContext(SecurityContext context) {
  39 + Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
  40 + contextHolder.set(context);
  41 + }
  42 +
  43 + @Override
  44 + public SecurityContext createEmptyContext() {
  45 + return new SecurityContextImpl();
  46 + }
  47 +
  48 +}
... ...
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 {
95 95 loginUser, null, loginUser.getAuthorities());
96 96 authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
97 97 // 设置到上下文
98   - //何时调用 SecurityContextHolder.clearContext. spring security filter 应该会调用 clearContext
99 98 SecurityContextHolder.getContext().setAuthentication(authenticationToken);
100 99 // 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号;
101 100 // 原因是,Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息
102 101 WebFrameworkUtils.setLoginUserId(request, loginUser.getId());
103 102 WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType());
104 103 // TODO @jason:使用 userId 会不会更合适哈?
  104 + // TODO @芋艿:activiti 需要使用 ttl 上下文
  105 + // TODO @jason:清理问题
105 106 if (Objects.equals(UserTypeEnum.ADMIN.getValue(), loginUser.getUserType())) {
106 107 org.activiti.engine.impl.identity.Authentication.setAuthenticatedUserId(loginUser.getUsername());
107 108 }
... ...
yudao-framework/yudao-spring-boot-starter-tenant/pom.xml
... ... @@ -32,6 +32,12 @@
32 32 <groupId>cn.iocoder.boot</groupId>
33 33 <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
34 34 </dependency>
  35 +
  36 + <!-- Job 定时任务相关 -->
  37 + <dependency>
  38 + <groupId>cn.iocoder.boot</groupId>
  39 + <artifactId>yudao-spring-boot-starter-job</artifactId>
  40 + </dependency>
35 41 </dependencies>
36 42  
37 43 </project>
... ...
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantJobAutoConfiguration.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.tenant.config;
  2 +
  3 +import cn.hutool.core.annotation.AnnotationUtil;
  4 +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
  5 +import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
  6 +import cn.iocoder.yudao.framework.tenant.core.job.TenantJobHandlerDecorator;
  7 +import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
  8 +import org.springframework.beans.BeansException;
  9 +import org.springframework.beans.factory.config.BeanPostProcessor;
  10 +import org.springframework.context.annotation.Bean;
  11 +
  12 +/**
  13 + * 多租户针对 Job 的自动配置
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +public class YudaoTenantJobAutoConfiguration {
  18 +
  19 + @Bean
  20 + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
  21 + public BeanPostProcessor jobHandlerBeanPostProcessor(TenantFrameworkService tenantFrameworkService) {
  22 + return new BeanPostProcessor() {
  23 +
  24 + @Override
  25 + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
  26 + if (!(bean instanceof JobHandler)) {
  27 + return bean;
  28 + }
  29 + // 有 TenantJob 注解的情况下,才会进行处理
  30 + if (!AnnotationUtil.hasAnnotation(bean.getClass(), TenantJob.class)) {
  31 + return bean;
  32 + }
  33 +
  34 + // 使用 TenantJobHandlerDecorator 装饰
  35 + return new TenantJobHandlerDecorator(tenantFrameworkService, (JobHandler) bean);
  36 + }
  37 +
  38 + };
  39 + }
  40 +
  41 +}
... ...
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJob.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.tenant.core.job;
  2 +
  3 +import java.lang.annotation.ElementType;
  4 +import java.lang.annotation.Retention;
  5 +import java.lang.annotation.RetentionPolicy;
  6 +import java.lang.annotation.Target;
  7 +
  8 +/**
  9 + * 多租户 Job 注解
  10 + */
  11 +@Target({ElementType.TYPE})
  12 +@Retention(RetentionPolicy.RUNTIME)
  13 +public @interface TenantJob {
  14 +}
... ...
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerDecorator.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.tenant.core.job;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  5 +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
  6 +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
  7 +import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
  8 +import lombok.AllArgsConstructor;
  9 +
  10 +import java.util.List;
  11 +import java.util.Map;
  12 +import java.util.concurrent.ConcurrentHashMap;
  13 +
  14 +/**
  15 + * 多租户 JobHandler 装饰器
  16 + * 任务执行时,会按照租户逐个执行 Job 的逻辑
  17 + *
  18 + * 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。
  19 + *
  20 + * @author 芋道源码
  21 + */
  22 +@AllArgsConstructor
  23 +public class TenantJobHandlerDecorator implements JobHandler {
  24 +
  25 + private final TenantFrameworkService tenantFrameworkService;
  26 + /**
  27 + * 被装饰的 Job
  28 + */
  29 + private final JobHandler jobHandler;
  30 +
  31 + @Override
  32 + public final String execute(String param) throws Exception {
  33 + // 获得租户列表
  34 + List<Long> tenantIds = tenantFrameworkService.getTenantIds();
  35 + if (CollUtil.isEmpty(tenantIds)) {
  36 + return null;
  37 + }
  38 +
  39 + // 逐个租户,执行 Job
  40 + Map<Long, String> results = new ConcurrentHashMap<>();
  41 + tenantIds.parallelStream().forEach(tenantId -> { // TODO 芋艿:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况
  42 + try {
  43 + // 设置租户
  44 + TenantContextHolder.setTenantId(tenantId);
  45 + // 执行 Job
  46 + String result = jobHandler.execute(param);
  47 + // 添加结果
  48 + results.put(tenantId, result);
  49 + } catch (Exception e) {
  50 + throw new RuntimeException(e);
  51 + } finally {
  52 + TenantContextHolder.clear();
  53 + }
  54 + });
  55 + return JsonUtils.toJsonString(results);
  56 + }
  57 +
  58 +}
... ...
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerInvoker.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.tenant.core.job;
  2 +
  3 +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandlerInvoker;
  4 +
  5 +/**
  6 + * 多租户 JobHandlerInvoker 拓展实现类
  7 + *
  8 + * @author 芋道源码
  9 + */
  10 +public class TenantJobHandlerInvoker extends JobHandlerInvoker {
  11 +
  12 +
  13 +
  14 +}
... ...
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/service/TenantFrameworkService.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.tenant.core.service;
  2 +
  3 +import java.util.List;
  4 +
  5 +/**
  6 + * Tenant 框架 Service 接口,定义获取租户信息
  7 + *
  8 + * @author 芋道源码
  9 + */
  10 +public interface TenantFrameworkService {
  11 +
  12 + /**
  13 + * 获得所有租户
  14 + *
  15 + * @return 租户编号数组
  16 + */
  17 + List<Long> getTenantIds();
  18 +
  19 +}
... ...
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;
12 12  
13 13 /**
14 14 * 多租户 Web 过滤器
15   - * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号
  15 + * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。
  16 + *
  17 + * Q:会不会存在模拟 tenant-id 导致跨租户的问题?
  18 + * A:用户登陆后,获得的 Token 是基于租户级别隔离,从而保证授权失败。
16 19 *
17 20 * @author 芋道源码
18 21 */
... ...
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java
1 1 /**
2 2 * 多租户,支持如下层面:
3   - * 1. DB:基于 MyBatis Plus 多租户的功能实现
4   - * 2. Job:TODO
5   - * 3. MQ:TODO
6   - * 4. Web:TODO
  3 + * 1. DB:基于 MyBatis Plus 多租户的功能实现。
  4 + * 2. Web:请求 HTTP API 时,Header 带上 tenant-id 租户编号。
  5 + * 3. Job:在 JobHandler 执行任务时,会按照每个租户,都独立并行执行一次。
  6 + * 4. MQ:TODO
7 7 */
8 8 package cn.iocoder.yudao.framework.tenant;
... ...
yudao-framework/yudao-spring-boot-starter-tenant/src/main/resources/META-INF/spring.factories
1 1 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
2 2 cn.iocoder.yudao.framework.tenant.config.YudaoTenantDatabaseAutoConfiguration,\
3   - cn.iocoder.yudao.framework.tenant.config.YudaoTenantWebAutoConfiguration
  3 + cn.iocoder.yudao.framework.tenant.config.YudaoTenantWebAutoConfiguration,\
  4 + cn.iocoder.yudao.framework.tenant.config.YudaoTenantJobAutoConfiguration
... ...