增加 Tenant Redis 的实现

This commit is contained in:
YunaiV 2021-12-06 10:18:36 +08:00
parent 1ce2c09f47
commit df9b06843f
9 changed files with 117 additions and 16 deletions

View File

@ -17,7 +17,7 @@ public interface WebFilterOrderEnum {
// OrderedRequestContextFilter 默认为 -105用于国际化上下文等等 // OrderedRequestContextFilter 默认为 -105用于国际化上下文等等
int TENANT_FILTER = - 100; // 需要保证在 ApiAccessLogFilter 前面 int TENANT_CONTEXT_FILTER = - 100; // 需要保证在 ApiAccessLogFilter 前面
int API_ACCESS_LOG_FILTER = -90; // 需要保证在 RequestBodyCacheFilter 后面 int API_ACCESS_LOG_FILTER = -90; // 需要保证在 RequestBodyCacheFilter 后面

View File

@ -98,4 +98,16 @@ public class RedisKeyDefine {
this(memo, keyTemplate, keyType, valueType, timeoutType, Duration.ZERO); 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);
}
} }

View File

@ -28,10 +28,6 @@ public class LoginUser implements UserDetails {
* 关联 {@link UserTypeEnum} * 关联 {@link UserTypeEnum}
*/ */
private Integer userType; private Integer userType;
/**
* 部门编号
*/
private Long deptId;
/** /**
* 角色编号数组 * 角色编号数组
*/ */
@ -53,22 +49,28 @@ public class LoginUser implements UserDetails {
* 状态 * 状态
*/ */
private Integer status; private Integer status;
/**
* 租户编号
*/
private Long tenantId;
// ========== UserTypeEnum.ADMIN 独有字段 ==========
// TODO 芋艿可以通过定义一个 Map<String, String> exts 的方式去除管理员的字段不过这样会导致系统比较复杂所以暂时不去掉先
/**
* 部门编号
*/
private Long deptId;
/** /**
* 所属岗位 * 所属岗位
*/ */
private Set<Long> postIds; private Set<Long> postIds;
/** /**
* group 目前指岗位代替 * group 目前指岗位代替
*/ */
// TODO jason这个字段改成 postCodes 明确更好哈 // TODO jason这个字段改成 postCodes 明确更好哈
private List<String> groups; private List<String> groups;
// TODO @芋艿怎么去掉 deptId
@Override @Override
@JsonIgnore// 避免序列化 @JsonIgnore// 避免序列化
public String getPassword() { public String getPassword() {

View File

@ -33,6 +33,11 @@
<artifactId>yudao-spring-boot-starter-mybatis</artifactId> <artifactId>yudao-spring-boot-starter-mybatis</artifactId>
</dependency> </dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-redis</artifactId>
</dependency>
<!-- Job 定时任务相关 --> <!-- Job 定时任务相关 -->
<dependency> <dependency>
<groupId>cn.iocoder.boot</groupId> <groupId>cn.iocoder.boot</groupId>
@ -44,6 +49,13 @@
<groupId>cn.iocoder.boot</groupId> <groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-mq</artifactId> <artifactId>yudao-spring-boot-starter-mq</artifactId>
</dependency> </dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.framework.tenant.config; package cn.iocoder.yudao.framework.tenant.config;
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; 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.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -13,10 +13,10 @@ import org.springframework.context.annotation.Bean;
public class YudaoTenantWebAutoConfiguration { public class YudaoTenantWebAutoConfiguration {
@Bean @Bean
public FilterRegistrationBean<TenantWebFilter> tenantWebFilter() { public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter() {
FilterRegistrationBean<TenantWebFilter> registrationBean = new FilterRegistrationBean<>(); FilterRegistrationBean<TenantContextWebFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new TenantWebFilter()); registrationBean.setFilter(new TenantContextWebFilter());
registrationBean.setOrder(WebFilterOrderEnum.TENANT_FILTER); registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER);
return registrationBean; return registrationBean;
} }

View File

@ -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);
}
}

View File

@ -11,7 +11,7 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
/** /**
* 多租户 Web 过滤器 * 多租户 Context Web 过滤器
* 将请求 Header 中的 tenant-id 解析出来添加到 {@link TenantContextHolder} 这样后续的 DB 等操作可以获得到租户编号 * 将请求 Header 中的 tenant-id 解析出来添加到 {@link TenantContextHolder} 这样后续的 DB 等操作可以获得到租户编号
* *
* Q会不会存在模拟 tenant-id 导致跨租户的问题 * Q会不会存在模拟 tenant-id 导致跨租户的问题
@ -19,7 +19,7 @@ import java.io.IOException;
* *
* @author 芋道源码 * @author 芋道源码
*/ */
public class TenantWebFilter extends OncePerRequestFilter { public class TenantContextWebFilter extends OncePerRequestFilter {
private static final String HEADER_TENANT_ID = "tenant-id"; private static final String HEADER_TENANT_ID = "tenant-id";

View File

@ -10,5 +10,6 @@
* 2Spring Security * 2Spring Security
* TransmittableThreadLocalSecurityContextHolderStrategy * TransmittableThreadLocalSecurityContextHolderStrategy
* YudaoSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法 * YudaoSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法
* 6. Redis通过在 Redis Key 上拼接租户编号的方式进行隔离
*/ */
package cn.iocoder.yudao.framework.tenant; package cn.iocoder.yudao.framework.tenant;

View File

@ -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);
}
}