From df9b06843f6b88bb02563568501ef6b38c006b6c Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 6 Dec 2021 10:18:36 +0800 Subject: [PATCH] 增加 Tenant Redis 的实现 --- yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/enums/WebFilterOrderEnum.java | 2 +- yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/RedisKeyDefine.java | 12 ++++++++++++ yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java | 18 ++++++++++-------- yudao-framework/yudao-spring-boot-starter-tenant/pom.xml | 12 ++++++++++++ yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantWebAutoConfiguration.java | 10 +++++----- yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java | 47 +++++++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java | 42 ++++++++++++++++++++++++++++++++++++++++++ yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantWebFilter.java | 42 ------------------------------------------ yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/package-info.java | 1 + yudao-framework/yudao-spring-boot-starter-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java | 27 +++++++++++++++++++++++++++ 10 files changed, 157 insertions(+), 56 deletions(-) create mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java create mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantWebFilter.java 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 1d8cc40..9a8acca 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 1167bfa..ba4fccb 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 0090e66..91db0b5 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 @@ -29,10 +29,6 @@ public class LoginUser implements UserDetails { */ private Integer userType; /** - * 部门编号 - */ - private Long deptId; - /** * 角色编号数组 */ private Set roleIds; @@ -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 23d8978..bc43028 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 7b2e1aa..faacbc4 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 0000000..f90ac12 --- /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/TenantContextWebFilter.java b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java new file mode 100644 index 0000000..ac43dca --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantContextWebFilter.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.framework.tenant.core.web; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.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 等操作,可以获得到租户编号。 + * + * Q:会不会存在模拟 tenant-id 导致跨租户的问题? + * A:用户登陆后,获得的 Token 是基于租户级别隔离,从而保证授权失败。 + * + * @author 芋道源码 + */ +public class TenantContextWebFilter extends OncePerRequestFilter { + + private static final String HEADER_TENANT_ID = "tenant-id"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + // 设置 + String tenantId = request.getHeader(HEADER_TENANT_ID); + if (StrUtil.isNotEmpty(tenantId)) { + TenantContextHolder.setTenantId(Long.valueOf(tenantId)); + } + try { + chain.doFilter(request, response); + } finally { + // 清理 + TenantContextHolder.clear(); + } + } + +} 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/TenantWebFilter.java deleted file mode 100644 index 40360e3..0000000 --- a/yudao-framework/yudao-spring-boot-starter-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/web/TenantWebFilter.java +++ /dev/null @@ -1,42 +0,0 @@ -package cn.iocoder.yudao.framework.tenant.core.web; - -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.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; - -/** - * 多租户 Web 过滤器 - * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。 - * - * Q:会不会存在模拟 tenant-id 导致跨租户的问题? - * A:用户登陆后,获得的 Token 是基于租户级别隔离,从而保证授权失败。 - * - * @author 芋道源码 - */ -public class TenantWebFilter extends OncePerRequestFilter { - - private static final String HEADER_TENANT_ID = "tenant-id"; - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) - throws ServletException, IOException { - // 设置 - String tenantId = request.getHeader(HEADER_TENANT_ID); - if (StrUtil.isNotEmpty(tenantId)) { - TenantContextHolder.setTenantId(Long.valueOf(tenantId)); - } - try { - chain.doFilter(request, response); - } finally { - // 清理 - TenantContextHolder.clear(); - } - } - -} 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 0856628..90420e0 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 0000000..d456e01 --- /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); + } + +} -- libgit2 0.21.4