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

pull/2/head
YunaiV 2021-12-05 10:44:17 +08:00
parent 535d3c9c01
commit 6cd9b3bf7e
19 changed files with 327 additions and 10 deletions

View File

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

View File

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

View File

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

View File

@ -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;
}

View File

@ -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> {
}

View File

@ -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 {
}

View File

@ -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);
}
}

View File

@ -1,15 +1,18 @@
package cn.iocoder.yudao.framework.security.config; package cn.iocoder.yudao.framework.security.config;
import cn.iocoder.yudao.framework.security.core.aop.PreAuthenticatedAspect; 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.filter.JWTAuthenticationTokenFilter;
import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl; 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.AuthenticationEntryPointImpl;
import cn.iocoder.yudao.framework.security.core.handler.LogoutSuccessHandlerImpl; import cn.iocoder.yudao.framework.security.core.handler.LogoutSuccessHandlerImpl;
import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService; import cn.iocoder.yudao.framework.security.core.service.SecurityAuthFrameworkService;
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; 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.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.AuthenticationEntryPoint;
@ -85,4 +88,17 @@ public class YudaoSecurityAutoConfiguration {
return new JWTAuthenticationTokenFilter(securityProperties, securityFrameworkService, globalExceptionHandler); 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;
}
} }

View File

@ -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();
}
}

View File

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

View File

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

View File

@ -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);
}
};
}
}

View File

@ -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 {
}

View File

@ -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);
}
}

View File

@ -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 {
}

View File

@ -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();
}

View File

@ -12,7 +12,10 @@ import java.io.IOException;
/** /**
* Web * Web
* Header tenant-id {@link TenantContextHolder} DB * Header tenant-id {@link TenantContextHolder} DB
*
* Q tenant-id
* A Token
* *
* @author * @author
*/ */

View File

@ -1,8 +1,8 @@
/** /**
* *
* 1. DB MyBatis Plus * 1. DB MyBatis Plus
* 2. JobTODO * 2. Web HTTP API Header tenant-id
* 3. MQTODO * 3. Job JobHandler
* 4. WebTODO * 4. MQTODO
*/ */
package cn.iocoder.yudao.framework.tenant; package cn.iocoder.yudao.framework.tenant;

View File

@ -1,3 +1,4 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.yudao.framework.tenant.config.YudaoTenantDatabaseAutoConfiguration,\ 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