mirror of
https://gitee.com/hhyykk/ipms-sjy.git
synced 2025-07-25 00:15:06 +08:00
1. 增加 Job 的多租户的能力
This commit is contained in:
@ -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
|
||||
|
Reference in New Issue
Block a user