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 17  
18 18 // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等
19 19  
20   - int TENANT_FILTER = - 100; // 需要保证在 ApiAccessLogFilter 前面
  20 + int TENANT_CONTEXT_FILTER = - 100; // 需要保证在 ApiAccessLogFilter 前面
21 21  
22 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 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 29 */
30 30 private Integer userType;
31 31 /**
32   - * 部门编号
33   - */
34   - private Long deptId;
35   - /**
36 32 * 角色编号数组
37 33 */
38 34 private Set<Long> roleIds;
... ... @@ -53,22 +49,28 @@ public class LoginUser implements UserDetails {
53 49 * 状态
54 50 */
55 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 67 private Set<Long> postIds;
62   -
63 68 /**
64 69 * group 目前指岗位代替
65 70 */
66 71 // TODO jason:这个字段,改成 postCodes 明确更好哈
67 72 private List<String> groups;
68 73  
69   -
70   - // TODO @芋艿:怎么去掉 deptId
71   -
72 74 @Override
73 75 @JsonIgnore// 避免序列化
74 76 public String getPassword() {
... ...
yudao-framework/yudao-spring-boot-starter-tenant/pom.xml
... ... @@ -33,6 +33,11 @@
33 33 <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
34 34 </dependency>
35 35  
  36 + <dependency>
  37 + <groupId>cn.iocoder.boot</groupId>
  38 + <artifactId>yudao-spring-boot-starter-redis</artifactId>
  39 + </dependency>
  40 +
36 41 <!-- Job 定时任务相关 -->
37 42 <dependency>
38 43 <groupId>cn.iocoder.boot</groupId>
... ... @@ -44,6 +49,13 @@
44 49 <groupId>cn.iocoder.boot</groupId>
45 50 <artifactId>yudao-spring-boot-starter-mq</artifactId>
46 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 59 </dependencies>
48 60  
49 61 </project>
... ...
yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java
1 1 package cn.iocoder.yudao.framework.tenant.config;
2 2  
3 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 5 import org.springframework.boot.web.servlet.FilterRegistrationBean;
6 6 import org.springframework.context.annotation.Bean;
7 7  
... ... @@ -13,10 +13,10 @@ import org.springframework.context.annotation.Bean;
13 13 public class YudaoTenantWebAutoConfiguration {
14 14  
15 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 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 11 import java.io.IOException;
12 12  
13 13 /**
14   - * 多租户 Web 过滤器
  14 + * 多租户 Context Web 过滤器
15 15 * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。
16 16 *
17 17 * Q:会不会存在模拟 tenant-id 导致跨租户的问题?
... ... @@ -19,7 +19,7 @@ import java.io.IOException;
19 19 *
20 20 * @author 芋道源码
21 21 */
22   -public class TenantWebFilter extends OncePerRequestFilter {
  22 +public class TenantContextWebFilter extends OncePerRequestFilter {
23 23  
24 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 10 * 2)Spring Security:
11 11 * TransmittableThreadLocalSecurityContextHolderStrategy
12 12 * 和 YudaoSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法
  13 + * 6. Redis:通过在 Redis Key 上拼接租户编号的方式,进行隔离。
13 14 */
14 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 +}
... ...