Browse Source

增加 Tenant Redis 的实现

YunaiV 3 years ago
parent
commit
df9b06843f

+ 1 - 1
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java

@@ -17,7 +17,7 @@ public interface WebFilterOrderEnum {
 
     // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等
 
-    int TENANT_FILTER = - 100; // 需要保证在 ApiAccessLogFilter 前面
+    int TENANT_CONTEXT_FILTER = - 100; // 需要保证在 ApiAccessLogFilter 前面
 
     int API_ACCESS_LOG_FILTER = -90; // 需要保证在 RequestBodyCacheFilter 后面
 

+ 12 - 0
yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/RedisKeyDefine.java

@@ -98,4 +98,16 @@ public class RedisKeyDefine {
         this(memo, keyTemplate, keyType, valueType, timeoutType, Duration.ZERO);
     }
 
+    /**
+     * 格式化 Key
+     *
+     * 注意,内部采用 {@link String#format(String, Object...)} 实现
+     *
+     * @param args 格式化的参数
+     * @return Key
+     */
+    public String formatKey(Object... args) {
+        return String.format(keyTemplate, args);
+    }
+
 }

+ 10 - 8
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java

@@ -28,10 +28,6 @@ public class LoginUser implements UserDetails {
      * 关联 {@link UserTypeEnum}
      */
     private Integer userType;
-    /**
-     * 部门编号
-     */
-    private Long deptId;
     /**
      * 角色编号数组
      */
@@ -53,22 +49,28 @@ public class LoginUser implements UserDetails {
      * 状态
      */
     private Integer status;
+    /**
+     * 租户编号
+     */
+    private Long tenantId;
 
+    // ========== UserTypeEnum.ADMIN 独有字段 ==========
+    // TODO 芋艿:可以通过定义一个 Map<String, String> exts 的方式,去除管理员的字段。不过这样会导致系统比较复杂,所以暂时不去掉先;
 
+    /**
+     * 部门编号
+     */
+    private Long deptId;
     /**
      * 所属岗位
      */
     private Set<Long> postIds;
-
     /**
      * group  目前指岗位代替
      */
     // TODO jason:这个字段,改成 postCodes 明确更好哈
     private List<String> groups;
 
-
-    // TODO @芋艿:怎么去掉 deptId
-
     @Override
     @JsonIgnore// 避免序列化
     public String getPassword() {

+ 12 - 0
yudao-framework/yudao-spring-boot-starter-tenant/pom.xml

@@ -33,6 +33,11 @@
             <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-redis</artifactId>
+        </dependency>
+
         <!-- Job 定时任务相关 -->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
@@ -44,6 +49,13 @@
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-spring-boot-starter-mq</artifactId>
         </dependency>
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
 </project>

+ 5 - 5
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java

@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.framework.tenant.config;
 
 import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
-import cn.iocoder.yudao.framework.tenant.core.web.TenantWebFilter;
+import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter;
 import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.context.annotation.Bean;
 
@@ -13,10 +13,10 @@ import org.springframework.context.annotation.Bean;
 public class YudaoTenantWebAutoConfiguration {
 
     @Bean
-    public FilterRegistrationBean<TenantWebFilter> tenantWebFilter() {
-        FilterRegistrationBean<TenantWebFilter> registrationBean = new FilterRegistrationBean<>();
-        registrationBean.setFilter(new TenantWebFilter());
-        registrationBean.setOrder(WebFilterOrderEnum.TENANT_FILTER);
+    public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter() {
+        FilterRegistrationBean<TenantContextWebFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(new TenantContextWebFilter());
+        registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER);
         return registrationBean;
     }
 

+ 47 - 0
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java

@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.framework.tenant.core.redis;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
+
+import java.time.Duration;
+
+/**
+ * 多租户拓展的 RedisKeyDefine 实现类
+ *
+ * 由于 Redis 不同于 MySQL  有 column 字段,所以无法通过类似 WHERE tenant_id = ? 的方式过滤
+ * 所以需要通过在 Redis Key 上增加后缀的方式,进行租户之间的隔离。具体的步骤是:
+ * 1. 假设 Redis Key 是 user:%d,示例是 user:1;对应到多租户的 Redis Key 是 user:%d:%d,
+ * 2. 在 Redis DAO 中,需要使用 {@link #formatKey(Object...)} 方法,进行 Redis Key 的格式化
+ *
+ * 注意,大多数情况下,并不用使用 TenantRedisKeyDefine 实现。主要的使用场景,是 Redis Key 可能存在冲突的情况。
+ * 例如说,租户 1 和 2 都有一个手机号作为 Key,则他们会存在冲突的问题
+ *
+ * @author 芋道源码
+ */
+public class TenantRedisKeyDefine extends RedisKeyDefine {
+
+    /**
+     * 多租户的 KEY 模板
+     */
+    private static final String KEY_TEMPLATE_SUFFIX = ":%d";
+
+    public TenantRedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, Duration timeout) {
+        super(memo, buildKeyTemplate(keyTemplate), keyType, valueType, timeout);
+    }
+
+    public TenantRedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, TimeoutTypeEnum timeoutType) {
+        super(memo, buildKeyTemplate(keyTemplate), keyType, valueType, timeoutType);
+    }
+
+    private static String buildKeyTemplate(String keyTemplate) {
+        return keyTemplate + KEY_TEMPLATE_SUFFIX;
+    }
+
+    @Override
+    public String formatKey(Object... args) {
+        args = ArrayUtil.append(args, TenantContextHolder.getTenantId());
+        return super.formatKey(args);
+    }
+
+}

+ 2 - 2
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantWebFilter.java → yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java

@@ -11,7 +11,7 @@ import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 
 /**
- * 多租户 Web 过滤器
+ * 多租户 Context Web 过滤器
  * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。
  *
  * Q:会不会存在模拟 tenant-id 导致跨租户的问题?
@@ -19,7 +19,7 @@ import java.io.IOException;
  *
  * @author 芋道源码
  */
-public class TenantWebFilter extends OncePerRequestFilter {
+public class TenantContextWebFilter extends OncePerRequestFilter {
 
     private static final String HEADER_TENANT_ID = "tenant-id";
 

+ 1 - 0
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java

@@ -10,5 +10,6 @@
  *      2)Spring Security:
  *          TransmittableThreadLocalSecurityContextHolderStrategy
  *          和 YudaoSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法
+ * 6. Redis:通过在 Redis Key 上拼接租户编号的方式,进行隔离。
  */
 package cn.iocoder.yudao.framework.tenant;

+ 27 - 0
yudao-framework/yudao-spring-boot-starter-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.framework.tenant.core.redis;
+
+import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class TenantRedisKeyDefineTest {
+
+    @Test
+    public void testFormatKey() {
+        Long tenantId = 30L;
+        TenantContextHolder.setTenantId(tenantId);
+        // 准备参数
+        TenantRedisKeyDefine define = new TenantRedisKeyDefine("", "user:%d:%d", RedisKeyDefine.KeyTypeEnum.HASH,
+                Object.class, RedisKeyDefine.TimeoutTypeEnum.FIXED);
+        Long userId = 10L;
+        Integer userType = 1;
+
+        // 调用
+        String key = define.formatKey(userId, userType);
+        // 断言
+        assertEquals("user:10:1:30", key);
+    }
+
+}