Commit df9b06843f6b88bb02563568501ef6b38c006b6c

Authored by YunaiV
1 parent 1ce2c09f

增加 Tenant Redis 的实现

yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java
@@ -17,7 +17,7 @@ public interface WebFilterOrderEnum { @@ -17,7 +17,7 @@ public interface WebFilterOrderEnum {
17 17
18 // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等 18 // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等
19 19
20 - int TENANT_FILTER = - 100; // 需要保证在 ApiAccessLogFilter 前面 20 + int TENANT_CONTEXT_FILTER = - 100; // 需要保证在 ApiAccessLogFilter 前面
21 21
22 int API_ACCESS_LOG_FILTER = -90; // 需要保证在 RequestBodyCacheFilter 后面 22 int API_ACCESS_LOG_FILTER = -90; // 需要保证在 RequestBodyCacheFilter 后面
23 23
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 { @@ -98,4 +98,16 @@ public class RedisKeyDefine {
98 this(memo, keyTemplate, keyType, valueType, timeoutType, Duration.ZERO); 98 this(memo, keyTemplate, keyType, valueType, timeoutType, Duration.ZERO);
99 } 99 }
100 100
  101 + /**
  102 + * 格式化 Key
  103 + *
  104 + * 注意,内部采用 {@link String#format(String, Object...)} 实现
  105 + *
  106 + * @param args 格式化的参数
  107 + * @return Key
  108 + */
  109 + public String formatKey(Object... args) {
  110 + return String.format(keyTemplate, args);
  111 + }
  112 +
101 } 113 }
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java
@@ -29,10 +29,6 @@ public class LoginUser implements UserDetails { @@ -29,10 +29,6 @@ public class LoginUser implements UserDetails {
29 */ 29 */
30 private Integer userType; 30 private Integer userType;
31 /** 31 /**
32 - * 部门编号  
33 - */  
34 - private Long deptId;  
35 - /**  
36 * 角色编号数组 32 * 角色编号数组
37 */ 33 */
38 private Set<Long> roleIds; 34 private Set<Long> roleIds;
@@ -53,22 +49,28 @@ public class LoginUser implements UserDetails { @@ -53,22 +49,28 @@ public class LoginUser implements UserDetails {
53 * 状态 49 * 状态
54 */ 50 */
55 private Integer status; 51 private Integer status;
  52 + /**
  53 + * 租户编号
  54 + */
  55 + private Long tenantId;
56 56
  57 + // ========== UserTypeEnum.ADMIN 独有字段 ==========
  58 + // TODO 芋艿:可以通过定义一个 Map<String, String> exts 的方式,去除管理员的字段。不过这样会导致系统比较复杂,所以暂时不去掉先;
57 59
58 /** 60 /**
  61 + * 部门编号
  62 + */
  63 + private Long deptId;
  64 + /**
59 * 所属岗位 65 * 所属岗位
60 */ 66 */
61 private Set<Long> postIds; 67 private Set<Long> postIds;
62 -  
63 /** 68 /**
64 * group 目前指岗位代替 69 * group 目前指岗位代替
65 */ 70 */
66 // TODO jason:这个字段,改成 postCodes 明确更好哈 71 // TODO jason:这个字段,改成 postCodes 明确更好哈
67 private List<String> groups; 72 private List<String> groups;
68 73
69 -  
70 - // TODO @芋艿:怎么去掉 deptId  
71 -  
72 @Override 74 @Override
73 @JsonIgnore// 避免序列化 75 @JsonIgnore// 避免序列化
74 public String getPassword() { 76 public String getPassword() {
yudao-framework/yudao-spring-boot-starter-tenant/pom.xml
@@ -33,6 +33,11 @@ @@ -33,6 +33,11 @@
33 <artifactId>yudao-spring-boot-starter-mybatis</artifactId> 33 <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
34 </dependency> 34 </dependency>
35 35
  36 + <dependency>
  37 + <groupId>cn.iocoder.boot</groupId>
  38 + <artifactId>yudao-spring-boot-starter-redis</artifactId>
  39 + </dependency>
  40 +
36 <!-- Job 定时任务相关 --> 41 <!-- Job 定时任务相关 -->
37 <dependency> 42 <dependency>
38 <groupId>cn.iocoder.boot</groupId> 43 <groupId>cn.iocoder.boot</groupId>
@@ -44,6 +49,13 @@ @@ -44,6 +49,13 @@
44 <groupId>cn.iocoder.boot</groupId> 49 <groupId>cn.iocoder.boot</groupId>
45 <artifactId>yudao-spring-boot-starter-mq</artifactId> 50 <artifactId>yudao-spring-boot-starter-mq</artifactId>
46 </dependency> 51 </dependency>
  52 +
  53 + <!-- Test 测试相关 -->
  54 + <dependency>
  55 + <groupId>org.springframework.boot</groupId>
  56 + <artifactId>spring-boot-starter-test</artifactId>
  57 + <scope>test</scope>
  58 + </dependency>
47 </dependencies> 59 </dependencies>
48 60
49 </project> 61 </project>
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java
1 package cn.iocoder.yudao.framework.tenant.config; 1 package cn.iocoder.yudao.framework.tenant.config;
2 2
3 import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; 3 import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
4 -import cn.iocoder.yudao.framework.tenant.core.web.TenantWebFilter; 4 +import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter;
5 import org.springframework.boot.web.servlet.FilterRegistrationBean; 5 import org.springframework.boot.web.servlet.FilterRegistrationBean;
6 import org.springframework.context.annotation.Bean; 6 import org.springframework.context.annotation.Bean;
7 7
@@ -13,10 +13,10 @@ import org.springframework.context.annotation.Bean; @@ -13,10 +13,10 @@ import org.springframework.context.annotation.Bean;
13 public class YudaoTenantWebAutoConfiguration { 13 public class YudaoTenantWebAutoConfiguration {
14 14
15 @Bean 15 @Bean
16 - public FilterRegistrationBean<TenantWebFilter> tenantWebFilter() {  
17 - FilterRegistrationBean<TenantWebFilter> registrationBean = new FilterRegistrationBean<>();  
18 - registrationBean.setFilter(new TenantWebFilter());  
19 - registrationBean.setOrder(WebFilterOrderEnum.TENANT_FILTER); 16 + public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter() {
  17 + FilterRegistrationBean<TenantContextWebFilter> registrationBean = new FilterRegistrationBean<>();
  18 + registrationBean.setFilter(new TenantContextWebFilter());
  19 + registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER);
20 return registrationBean; 20 return registrationBean;
21 } 21 }
22 22
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.tenant.core.redis;
  2 +
  3 +import cn.hutool.core.util.ArrayUtil;
  4 +import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
  5 +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
  6 +
  7 +import java.time.Duration;
  8 +
  9 +/**
  10 + * 多租户拓展的 RedisKeyDefine 实现类
  11 + *
  12 + * 由于 Redis 不同于 MySQL 有 column 字段,所以无法通过类似 WHERE tenant_id = ? 的方式过滤
  13 + * 所以需要通过在 Redis Key 上增加后缀的方式,进行租户之间的隔离。具体的步骤是:
  14 + * 1. 假设 Redis Key 是 user:%d,示例是 user:1;对应到多租户的 Redis Key 是 user:%d:%d,
  15 + * 2. 在 Redis DAO 中,需要使用 {@link #formatKey(Object...)} 方法,进行 Redis Key 的格式化
  16 + *
  17 + * 注意,大多数情况下,并不用使用 TenantRedisKeyDefine 实现。主要的使用场景,是 Redis Key 可能存在冲突的情况。
  18 + * 例如说,租户 1 和 2 都有一个手机号作为 Key,则他们会存在冲突的问题
  19 + *
  20 + * @author 芋道源码
  21 + */
  22 +public class TenantRedisKeyDefine extends RedisKeyDefine {
  23 +
  24 + /**
  25 + * 多租户的 KEY 模板
  26 + */
  27 + private static final String KEY_TEMPLATE_SUFFIX = ":%d";
  28 +
  29 + public TenantRedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, Duration timeout) {
  30 + super(memo, buildKeyTemplate(keyTemplate), keyType, valueType, timeout);
  31 + }
  32 +
  33 + public TenantRedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, TimeoutTypeEnum timeoutType) {
  34 + super(memo, buildKeyTemplate(keyTemplate), keyType, valueType, timeoutType);
  35 + }
  36 +
  37 + private static String buildKeyTemplate(String keyTemplate) {
  38 + return keyTemplate + KEY_TEMPLATE_SUFFIX;
  39 + }
  40 +
  41 + @Override
  42 + public String formatKey(Object... args) {
  43 + args = ArrayUtil.append(args, TenantContextHolder.getTenantId());
  44 + return super.formatKey(args);
  45 + }
  46 +
  47 +}
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantWebFilter.java renamed to 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; @@ -11,7 +11,7 @@ import javax.servlet.http.HttpServletResponse;
11 import java.io.IOException; 11 import java.io.IOException;
12 12
13 /** 13 /**
14 - * 多租户 Web 过滤器 14 + * 多租户 Context Web 过滤器
15 * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。 15 * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。
16 * 16 *
17 * Q:会不会存在模拟 tenant-id 导致跨租户的问题? 17 * Q:会不会存在模拟 tenant-id 导致跨租户的问题?
@@ -19,7 +19,7 @@ import java.io.IOException; @@ -19,7 +19,7 @@ import java.io.IOException;
19 * 19 *
20 * @author 芋道源码 20 * @author 芋道源码
21 */ 21 */
22 -public class TenantWebFilter extends OncePerRequestFilter { 22 +public class TenantContextWebFilter extends OncePerRequestFilter {
23 23
24 private static final String HEADER_TENANT_ID = "tenant-id"; 24 private static final String HEADER_TENANT_ID = "tenant-id";
25 25
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java
@@ -10,5 +10,6 @@ @@ -10,5 +10,6 @@
10 * 2)Spring Security: 10 * 2)Spring Security:
11 * TransmittableThreadLocalSecurityContextHolderStrategy 11 * TransmittableThreadLocalSecurityContextHolderStrategy
12 * 和 YudaoSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法 12 * 和 YudaoSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法
  13 + * 6. Redis:通过在 Redis Key 上拼接租户编号的方式,进行隔离。
13 */ 14 */
14 package cn.iocoder.yudao.framework.tenant; 15 package cn.iocoder.yudao.framework.tenant;
yudao-framework/yudao-spring-boot-starter-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.tenant.core.redis;
  2 +
  3 +import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
  4 +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
  5 +import org.junit.jupiter.api.Test;
  6 +
  7 +import static org.junit.jupiter.api.Assertions.assertEquals;
  8 +
  9 +class TenantRedisKeyDefineTest {
  10 +
  11 + @Test
  12 + public void testFormatKey() {
  13 + Long tenantId = 30L;
  14 + TenantContextHolder.setTenantId(tenantId);
  15 + // 准备参数
  16 + TenantRedisKeyDefine define = new TenantRedisKeyDefine("", "user:%d:%d", RedisKeyDefine.KeyTypeEnum.HASH,
  17 + Object.class, RedisKeyDefine.TimeoutTypeEnum.FIXED);
  18 + Long userId = 10L;
  19 + Integer userType = 1;
  20 +
  21 + // 调用
  22 + String key = define.formatKey(userId, userType);
  23 + // 断言
  24 + assertEquals("user:10:1:30", key);
  25 + }
  26 +
  27 +}