From df9b06843f6b88bb02563568501ef6b38c006b6c Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 6 Dec 2021 10:18:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20Tenant=20Redis=20=E7=9A=84?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/enums/WebFilterOrderEnum.java | 2 +- .../framework/redis/core/RedisKeyDefine.java | 12 +++++ .../framework/security/core/LoginUser.java | 18 +++---- .../yudao-spring-boot-starter-tenant/pom.xml | 12 +++++ .../YudaoTenantWebAutoConfiguration.java | 10 ++-- .../core/redis/TenantRedisKeyDefine.java | 47 +++++++++++++++++++ ...ilter.java => TenantContextWebFilter.java} | 4 +- .../yudao/framework/tenant/package-info.java | 1 + .../core/redis/TenantRedisKeyDefineTest.java | 27 +++++++++++ 9 files changed, 117 insertions(+), 16 deletions(-) create mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java rename yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/{TenantWebFilter.java => TenantContextWebFilter.java} (93%) create mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java index 1d8cc4034..9a8acca80 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java +++ b/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 后面 diff --git a/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/RedisKeyDefine.java b/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/RedisKeyDefine.java index 1167bfa26..ba4fccb66 100644 --- a/yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/RedisKeyDefine.java +++ b/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); + } + } diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java index 0090e6633..91db0b568 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java +++ b/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 exts 的方式,去除管理员的字段。不过这样会导致系统比较复杂,所以暂时不去掉先; + /** + * 部门编号 + */ + private Long deptId; /** * 所属岗位 */ private Set postIds; - /** * group 目前指岗位代替 */ // TODO jason:这个字段,改成 postCodes 明确更好哈 private List groups; - - // TODO @芋艿:怎么去掉 deptId - @Override @JsonIgnore// 避免序列化 public String getPassword() { diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/pom.xml b/yudao-framework/yudao-spring-boot-starter-tenant/pom.xml index 23d897828..bc4302809 100644 --- a/yudao-framework/yudao-spring-boot-starter-tenant/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-tenant/pom.xml @@ -33,6 +33,11 @@ yudao-spring-boot-starter-mybatis + + cn.iocoder.boot + yudao-spring-boot-starter-redis + + cn.iocoder.boot @@ -44,6 +49,13 @@ cn.iocoder.boot yudao-spring-boot-starter-mq + + + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java index 7b2e1aaf5..faacbc486 100644 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java +++ b/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() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); - registrationBean.setFilter(new TenantWebFilter()); - registrationBean.setOrder(WebFilterOrderEnum.TENANT_FILTER); + public FilterRegistrationBean tenantContextWebFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new TenantContextWebFilter()); + registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER); return registrationBean; } diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java new file mode 100644 index 000000000..f90ac1246 --- /dev/null +++ b/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); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantWebFilter.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java similarity index 93% rename from yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantWebFilter.java rename to yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java index 40360e3fc..ac43dca61 100644 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantWebFilter.java +++ b/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"; diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java index 0856628b0..90420e01c 100644 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java +++ b/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; diff --git a/yudao-framework/yudao-spring-boot-starter-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java new file mode 100644 index 000000000..d456e011c --- /dev/null +++ b/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); + } + +}