Commit df9b06843f6b88bb02563568501ef6b38c006b6c
1 parent
1ce2c09f
增加 Tenant Redis 的实现
Showing
9 changed files
with
117 additions
and
16 deletions
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 | +} | ... | ... |