mirror of
https://gitee.com/huangge1199_admin/vue-pro.git
synced 2024-11-30 03:01:53 +08:00
1. 增加 Job 的多租户的能力
This commit is contained in:
parent
535d3c9c01
commit
6cd9b3bf7e
@ -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 {
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
}
|
@ -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> {
|
||||
}
|
@ -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 {
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@ -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 {
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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 {
|
||||
|
||||
|
||||
|
||||
}
|
@ -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();
|
||||
|
||||
}
|
@ -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 芋道源码
|
||||
*/
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user