增加 Tenant Redis 的实现
parent
1ce2c09f47
commit
df9b06843f
|
@ -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 后面
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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";
|
||||||
|
|
|
@ -10,5 +10,6 @@
|
||||||
* 2)Spring Security:
|
* 2)Spring Security:
|
||||||
* TransmittableThreadLocalSecurityContextHolderStrategy
|
* TransmittableThreadLocalSecurityContextHolderStrategy
|
||||||
* 和 YudaoSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法
|
* 和 YudaoSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法
|
||||||
|
* 6. Redis:通过在 Redis Key 上拼接租户编号的方式,进行隔离。
|
||||||
*/
|
*/
|
||||||
package cn.iocoder.yudao.framework.tenant;
|
package cn.iocoder.yudao.framework.tenant;
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue