Browse Source

多租户组件

yq 2 years ago
parent
commit
b93022a9fa
16 changed files with 635 additions and 2 deletions
  1. 9 2
      pom.xml
  2. 1 0
      usky-common/pom.xml
  3. 4 0
      usky-common/usky-common-core/pom.xml
  4. 25 0
      usky-common/usky-common-core/src/main/java/com/usky/common/core/util/CacheUtils.java
  5. 28 0
      usky-common/usky-common-tenant/pom.xml
  6. 41 0
      usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/config/TenantProperties.java
  7. 77 0
      usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/config/UskyTenantAutoConfiguration.java
  8. 18 0
      usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/core/aop/TenantIgnore.java
  9. 33 0
      usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/core/aop/TenantIgnoreAspect.java
  10. 66 0
      usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/core/context/TenantContextHolder.java
  11. 47 0
      usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/core/db/TenantDatabaseInterceptor.java
  12. 112 0
      usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/core/security/TenantSecurityWebFilter.java
  13. 28 0
      usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/core/service/TenantFrameworkService.java
  14. 62 0
      usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/core/service/TenantFrameworkServiceImpl.java
  15. 36 0
      usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/core/util/TenantUtils.java
  16. 48 0
      usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/core/web/TenantContextWebFilter.java

+ 9 - 2
pom.xml

@@ -56,11 +56,13 @@
         <poi.version>4.1.2</poi.version>
         <commons-collections.version>3.2.2</commons-collections.version>
         <transmittable-thread-local.version>2.12.2</transmittable-thread-local.version>
-        <mybatis-plus-boot-starter.version>3.3.0</mybatis-plus-boot-starter.version>
+        <mybatis-plus-boot-starter.version>3.4.0</mybatis-plus-boot-starter.version>
+        <mybatis-plus-generator.version>3.4.0</mybatis-plus-generator.version>
         <orika-core.version>1.5.4</orika-core.version>
 <!--        <commons-lang3.version>3.11</commons-lang3.version>-->
         <commons-beanutils.version>1.9.4</commons-beanutils.version>
         <guava.version>29.0-jre</guava.version>
+        <hutool.version>5.6.1</hutool.version>
     </properties>
 
     <!-- 依赖声明 -->
@@ -131,7 +133,7 @@
             <dependency>
                 <groupId>com.baomidou</groupId>
                 <artifactId>mybatis-plus-generator</artifactId>
-                <version>${mybatis-plus-boot-starter.version}</version>
+                <version>${mybatis-plus-generator.version}</version>
             </dependency>
 
             <!-- 代码生成使用模板 -->
@@ -310,6 +312,11 @@
                 <artifactId>druid-spring-boot-starter</artifactId>
                 <version>${druid.version}</version>
             </dependency>
+            <dependency>
+                <groupId>cn.hutool</groupId>
+                <artifactId>hutool-all</artifactId>
+                <version>${hutool.version}</version>
+            </dependency>
 
 
             <dependency>

+ 1 - 0
usky-common/pom.xml

@@ -57,5 +57,6 @@
         <module>usky-common-security</module>
         <module>usky-common-mybatis</module>
         <module>usky-common-redis</module>
+        <module>usky-common-tenant</module>
     </modules>
 </project>

+ 4 - 0
usky-common/usky-common-core/pom.xml

@@ -97,6 +97,10 @@
             <groupId>io.jsonwebtoken</groupId>
             <artifactId>jjwt</artifactId>
         </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+        </dependency>
 
     </dependencies>
 

+ 25 - 0
usky-common/usky-common-core/src/main/java/com/usky/common/core/util/CacheUtils.java

@@ -0,0 +1,25 @@
+package com.usky.common.core.util;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+
+import java.time.Duration;
+import java.util.concurrent.Executors;
+
+/**
+ * Cache 工具类
+ *
+ * @author yq
+ */
+public class CacheUtils {
+
+    public static <K, V> LoadingCache<K, V> buildAsyncReloadingCache(Duration duration, CacheLoader<K, V> loader) {
+        return CacheBuilder.newBuilder()
+                // 只阻塞当前数据加载线程,其他线程返回旧值
+                .refreshAfterWrite(duration)
+                // 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程
+                .build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool()));
+    }
+
+}

+ 28 - 0
usky-common/usky-common-tenant/pom.xml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>usky-common</artifactId>
+        <groupId>com.usky</groupId>
+        <version>0.0.1</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>usky-common-tenant</artifactId>
+
+
+    <dependencies>
+
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>usky-common-mybatis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>usky-common-security</artifactId>
+        </dependency>
+
+    </dependencies>
+</project>

+ 41 - 0
usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/config/TenantProperties.java

@@ -0,0 +1,41 @@
+package com.usky.common.tenant.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.Set;
+
+/**
+ * 多租户配置
+ *
+ * @author yq
+ */
+@ConfigurationProperties(prefix = "usky.tenant")
+@Data
+public class TenantProperties {
+
+    /**
+     * 租户是否开启
+     */
+    private static final Boolean ENABLE_DEFAULT = true;
+
+    /**
+     * 是否开启
+     */
+    private Boolean enable = ENABLE_DEFAULT;
+
+    /**
+     * 需要忽略多租户的请求
+     *
+     * 默认情况下,每个请求需要带上 tenant-id 的请求头。但是,部分请求是无需带上的,例如说短信回调、支付回调等 Open API!
+     */
+    private Set<String> ignoreUrls;
+
+    /**
+     * 需要忽略多租户的表
+     *
+     * 即默认所有表都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟
+     */
+    private Set<String> ignoreTables;
+
+}

+ 77 - 0
usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/config/UskyTenantAutoConfiguration.java

@@ -0,0 +1,77 @@
+package com.usky.common.tenant.config;
+
+
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
+import com.usky.common.tenant.core.aop.TenantIgnoreAspect;
+import com.usky.common.tenant.core.db.TenantDatabaseInterceptor;
+import com.usky.common.tenant.core.security.TenantSecurityWebFilter;
+import com.usky.common.tenant.core.service.TenantFrameworkService;
+import com.usky.common.tenant.core.service.TenantFrameworkServiceImpl;
+import com.usky.common.tenant.core.web.TenantContextWebFilter;
+import com.usky.system.RemoteTenantService;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Configuration
+@ConditionalOnProperty(prefix = "usky.tenant", value = "enable", matchIfMissing = true) // 允许使用 yudao.tenant.enable=false 禁用多租户
+@EnableConfigurationProperties(TenantProperties.class)
+public class UskyTenantAutoConfiguration {
+
+    @Bean
+    public TenantFrameworkService tenantFrameworkService(RemoteTenantService remoteTenantService) {
+        return new TenantFrameworkServiceImpl(remoteTenantService);
+    }
+
+    // ========== AOP ==========
+
+    @Bean
+    public TenantIgnoreAspect tenantIgnoreAspect() {
+        return new TenantIgnoreAspect();
+    }
+
+    // ========== DB ==========
+
+    @Bean
+    public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties,
+                                                                 MybatisPlusInterceptor interceptor) {
+        TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties));
+        // 添加到 interceptor 中
+        // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
+
+        List<InnerInterceptor> inners = new ArrayList<>(interceptor.getInterceptors());
+        inners.add(0, inner);
+        interceptor.setInterceptors(inners);
+
+        return inner;
+    }
+
+    // ========== WEB ==========
+
+    @Bean
+    public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter() {
+        FilterRegistrationBean<TenantContextWebFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(new TenantContextWebFilter());
+        registrationBean.setOrder(-99);
+        return registrationBean;
+    }
+
+    // ========== Security ==========
+
+    @Bean
+    public FilterRegistrationBean<TenantSecurityWebFilter> tenantSecurityWebFilter(TenantProperties tenantProperties,
+                                                                                   TenantFrameworkService tenantFrameworkService) {
+        FilterRegistrationBean<TenantSecurityWebFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(new TenantSecurityWebFilter(tenantProperties, tenantFrameworkService));
+        registrationBean.setOrder(-99);
+        return registrationBean;
+    }
+
+}

+ 18 - 0
usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/core/aop/TenantIgnore.java

@@ -0,0 +1,18 @@
+package com.usky.common.tenant.core.aop;
+
+import java.lang.annotation.*;
+
+/**
+ * 忽略租户,标记指定方法不进行租户的自动过滤
+ *
+ * 注意,只有 DB 的场景会过滤,其它场景暂时不过滤:
+ * 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的
+ * 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略
+ *
+ * @author yq
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface TenantIgnore {
+}

+ 33 - 0
usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/core/aop/TenantIgnoreAspect.java

@@ -0,0 +1,33 @@
+package com.usky.common.tenant.core.aop;
+
+
+import com.usky.common.tenant.core.context.TenantContextHolder;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+
+/**
+ * 忽略多租户的 Aspect,基于 {@link TenantIgnore} 注解实现,用于一些全局的逻辑。
+ * 例如说,一个定时任务,读取所有数据,进行处理。
+ * 又例如说,读取所有数据,进行缓存。
+ *
+ * @author yq
+ */
+@Aspect
+@Slf4j
+public class TenantIgnoreAspect {
+
+    @Around("@annotation(tenantIgnore)")
+    public Object around(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable {
+        Boolean oldIgnore = TenantContextHolder.isIgnore();
+        try {
+            TenantContextHolder.setIgnore(true);
+            // 执行逻辑
+            return joinPoint.proceed();
+        } finally {
+            TenantContextHolder.setIgnore(oldIgnore);
+        }
+    }
+
+}

+ 66 - 0
usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/core/context/TenantContextHolder.java

@@ -0,0 +1,66 @@
+package com.usky.common.tenant.core.context;
+
+import com.alibaba.ttl.TransmittableThreadLocal;
+
+/**
+ * 多租户上下文 Holder
+ *
+ * @author yq
+ */
+public class TenantContextHolder {
+
+    /**
+     * 当前租户编号
+     */
+    private static final ThreadLocal<Integer> TENANT_ID = new TransmittableThreadLocal<>();
+
+    /**
+     * 是否忽略租户
+     */
+    private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>();
+
+    /**
+     * 获得租户编号。
+     *
+     * @return 租户编号
+     */
+    public static Integer getTenantId() {
+        return TENANT_ID.get();
+    }
+
+    /**
+     * 获得租户编号。如果不存在,则抛出 NullPointerException 异常
+     *
+     * @return 租户编号
+     */
+    public static Integer getRequiredTenantId() {
+        Integer tenantId = getTenantId();
+        if (tenantId == null) {
+            throw new NullPointerException("TenantContextHolder 不存在租户编号");
+        }
+        return tenantId;
+    }
+
+    public static void setTenantId(Integer tenantId) {
+        TENANT_ID.set(tenantId);
+    }
+
+    public static void setIgnore(Boolean ignore) {
+        IGNORE.set(ignore);
+    }
+
+    /**
+     * 当前是否忽略租户
+     *
+     * @return 是否忽略
+     */
+    public static boolean isIgnore() {
+        return Boolean.TRUE.equals(IGNORE.get());
+    }
+
+    public static void clear() {
+        TENANT_ID.remove();
+        IGNORE.remove();
+    }
+
+}

+ 47 - 0
usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/core/db/TenantDatabaseInterceptor.java

@@ -0,0 +1,47 @@
+package com.usky.common.tenant.core.db;
+
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
+import com.usky.common.tenant.config.TenantProperties;
+import com.usky.common.tenant.core.context.TenantContextHolder;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+
+import java.util.HashSet;
+import java.util.Set;
+
+
+
+/**
+ * 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能
+ *
+ * @author yq
+ */
+public class TenantDatabaseInterceptor implements TenantLineHandler {
+
+    private final Set<String> ignoreTables = new HashSet<>();
+
+    public TenantDatabaseInterceptor(TenantProperties properties) {
+        // 不同 DB 下,大小写的习惯不同,所以需要都添加进去
+        properties.getIgnoreTables().forEach(table -> {
+            ignoreTables.add(table.toLowerCase());
+            ignoreTables.add(table.toUpperCase());
+        });
+        // 在 OracleKeyGenerator 中,生成主键时,会查询这个表,查询这个表后,会自动拼接 TENANT_ID 导致报错
+        ignoreTables.add("DUAL");
+    }
+
+    @Override
+    public Expression getTenantId() {
+        return new LongValue(TenantContextHolder.getRequiredTenantId());
+    }
+
+    @Override
+    public boolean ignoreTable(String tableName) {
+        // 情况一,全局忽略多租户
+        // 情况二,忽略多租户的表
+        return TenantContextHolder.isIgnore()
+            || CollUtil.contains(ignoreTables, tableName);
+    }
+
+}

+ 112 - 0
usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/core/security/TenantSecurityWebFilter.java

@@ -0,0 +1,112 @@
+package com.usky.common.tenant.core.security;
+
+import cn.hutool.core.collection.CollUtil;
+import com.alibaba.fastjson.JSON;
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.util.ServletUtils;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.common.tenant.config.TenantProperties;
+import com.usky.common.tenant.core.context.TenantContextHolder;
+import com.usky.common.tenant.core.service.TenantFrameworkService;
+import com.usky.system.model.LoginUser;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * 多租户 Security Web 过滤器
+ * 1. 如果是登陆的用户,校验是否有权限访问该租户,避免越权问题。
+ * 2. 如果请求未带租户的编号,检查是否是忽略的 URL,否则也不允许访问。
+ * 3. 校验租户是合法,例如说被禁用、到期
+ *
+ * 校验用户访问的租户,是否是其所在的租户,
+ *
+ * @author yq
+ */
+@Slf4j
+public class TenantSecurityWebFilter extends OncePerRequestFilter {
+
+    private final TenantProperties tenantProperties;
+
+    private final AntPathMatcher pathMatcher;
+
+    private final TenantFrameworkService tenantFrameworkService;
+
+    public TenantSecurityWebFilter(TenantProperties tenantProperties,
+                                   TenantFrameworkService tenantFrameworkService) {
+        this.tenantProperties = tenantProperties;
+        this.pathMatcher = new AntPathMatcher();
+        this.tenantFrameworkService = tenantFrameworkService;
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+        Integer tenantId = TenantContextHolder.getTenantId();
+        // 1. 登陆的用户,校验是否有权限访问该租户,避免越权问题。
+        LoginUser user = SecurityUtils.getLoginUser();
+        if (user != null) {
+            // 如果获取不到租户编号,则尝试使用登陆用户的租户编号
+            if (tenantId == null) {
+                tenantId = user.getTenantId();
+                TenantContextHolder.setTenantId(tenantId);
+            // 如果传递了租户编号,则进行比对租户编号,避免越权问题
+            } else if (!Objects.equals(user.getTenantId(), TenantContextHolder.getTenantId())) {
+                String msg = "您无权访问该租户的数据";
+                ServletUtils.renderString(response, JSON.toJSONString(ApiResult.error(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), msg)));
+                return;
+            }
+        }
+
+        // 如果非允许忽略租户的 URL,则校验租户是否合法
+        if (!isIgnoreUrl(request)) {
+            // 2. 如果请求未带租户的编号,不允许访问。
+            if (tenantId == null) {
+                String msg = "租户的请求未传递,请进行排查";
+                ServletUtils.renderString(response, JSON.toJSONString(ApiResult.error(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), msg)));
+                return;
+            }
+            // 3. 校验租户是合法,例如说被禁用、到期
+            try {
+                ApiResult<Void> validResult = tenantFrameworkService.validTenant(tenantId);
+                if (ApiResult.ResultStatus.ERROR.equals(validResult.getStatus())){
+                    ServletUtils.renderString(response, JSON.toJSONString(ApiResult.error(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), validResult.getMsg())));
+                }
+            } catch (Throwable ex) {
+                String msg = "系统异常"+ex.getMessage();
+                ServletUtils.renderString(response, JSON.toJSONString(ApiResult.error(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()), msg)));
+                return;
+            }
+        } else { // 如果是允许忽略租户的 URL,若未传递租户编号,则默认忽略租户编号,避免报错
+            if (tenantId == null) {
+                TenantContextHolder.setIgnore(true);
+            }
+        }
+
+        // 继续过滤
+        chain.doFilter(request, response);
+    }
+
+    private boolean isIgnoreUrl(HttpServletRequest request) {
+        // 快速匹配,保证性能
+        if (CollUtil.contains(tenantProperties.getIgnoreUrls(), request.getRequestURI())) {
+            return true;
+        }
+        // 逐个 Ant 路径匹配
+        for (String url : tenantProperties.getIgnoreUrls()) {
+            if (pathMatcher.match(url, request.getRequestURI())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}

+ 28 - 0
usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/core/service/TenantFrameworkService.java

@@ -0,0 +1,28 @@
+package com.usky.common.tenant.core.service;
+
+import com.usky.common.core.bean.ApiResult;
+
+import java.util.List;
+
+/**
+ * Tenant 框架 Service 接口,定义获取租户信息
+ *
+ * @author yq
+ */
+public interface TenantFrameworkService {
+
+    /**
+     * 获得所有租户
+     *
+     * @return 租户编号数组
+     */
+    ApiResult<List<Integer>> getTenantIds();
+
+    /**
+     * 校验租户是否合法
+     *
+     * @param id 租户编号
+     */
+    ApiResult<Void> validTenant(Integer id);
+
+}

+ 62 - 0
usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/core/service/TenantFrameworkServiceImpl.java

@@ -0,0 +1,62 @@
+package com.usky.common.tenant.core.service;
+
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.util.CacheUtils;
+import com.usky.system.RemoteTenantService;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+
+import java.time.Duration;
+import java.util.List;
+
+/**
+ * Tenant 框架 Service 实现类
+ *
+ * @author yq
+ */
+@RequiredArgsConstructor
+public class TenantFrameworkServiceImpl implements TenantFrameworkService {
+
+
+    private final RemoteTenantService remoteTenantService;
+
+    /**
+     * 针对 {@link #getTenantIds()} 的缓存
+     */
+    private final LoadingCache<Object, ApiResult<List<Integer>>> getTenantIdsCache = CacheUtils.buildAsyncReloadingCache(
+            Duration.ofMinutes(1L), // 过期时间 1 分钟
+            new CacheLoader<Object, ApiResult<List<Integer>>>() {
+                @Override
+                public ApiResult<List<Integer>> load(Object key) {
+                    return remoteTenantService.getTenantIds();
+                }
+            });
+
+    /**
+     * 针对校验结果的缓存
+      */
+    private final LoadingCache<Integer, ApiResult<Void>> validTenantCache = CacheUtils.buildAsyncReloadingCache(
+            Duration.ofMinutes(1L), // 过期时间 1 分钟
+            new CacheLoader<Integer, ApiResult<Void>>() {
+
+                @Override
+                public ApiResult<Void> load(Integer id) {
+                    return remoteTenantService.validTenant(id);
+                }
+
+            });
+
+    @Override
+    @SneakyThrows
+    public ApiResult<List<Integer>> getTenantIds() {
+        return getTenantIdsCache.get(Boolean.TRUE);
+    }
+
+    @Override
+    public ApiResult<Void> validTenant(Integer id) {
+        return validTenantCache.getUnchecked(id);
+    }
+
+}

+ 36 - 0
usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/core/util/TenantUtils.java

@@ -0,0 +1,36 @@
+package com.usky.common.tenant.core.util;
+
+
+import com.usky.common.tenant.core.context.TenantContextHolder;
+
+/**
+ * 多租户 Util
+ *
+ * @author yq
+ */
+public class TenantUtils {
+
+    /**
+     * 使用指定租户,执行对应的逻辑
+     *
+     * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户
+     * 当然,执行完成后,还是会恢复回去
+     *
+     * @param tenantId 租户编号
+     * @param runnable 逻辑
+     */
+    public static void execute(Integer tenantId, Runnable runnable) {
+        Integer oldTenantId = TenantContextHolder.getTenantId();
+        Boolean oldIgnore = TenantContextHolder.isIgnore();
+        try {
+            TenantContextHolder.setTenantId(tenantId);
+            TenantContextHolder.setIgnore(false);
+            // 执行逻辑
+            runnable.run();
+        } finally {
+            TenantContextHolder.setTenantId(oldTenantId);
+            TenantContextHolder.setIgnore(oldIgnore);
+        }
+    }
+
+}

+ 48 - 0
usky-common/usky-common-tenant/src/main/java/com/usky/common/tenant/core/web/TenantContextWebFilter.java

@@ -0,0 +1,48 @@
+package com.usky.common.tenant.core.web;
+
+import com.usky.common.core.util.StringUtils;
+import com.usky.common.tenant.core.context.TenantContextHolder;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * 多租户 Context Web 过滤器
+ * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。
+ *
+ * @author yq
+ */
+public class TenantContextWebFilter extends OncePerRequestFilter {
+
+
+    private final static String HEADER_TENANT_ID = "tenant-id";
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+        // 设置
+        Integer tenantId = getTenantId(request);
+        if (tenantId != null) {
+            TenantContextHolder.setTenantId(tenantId);
+        }
+        try {
+            chain.doFilter(request, response);
+        } finally {
+            // 清理
+            TenantContextHolder.clear();
+        }
+    }
+
+
+    /**
+     * 后续考虑设置web模块从web模块里面来获取
+     */
+    public static Integer getTenantId(HttpServletRequest request) {
+        String tenantId = request.getHeader(HEADER_TENANT_ID);
+        return StringUtils.isNotEmpty(tenantId) ? Integer.valueOf(tenantId) : null;
+    }
+
+}