增加 Tenant Security 的实现

This commit is contained in:
YunaiV 2021-12-06 13:25:33 +08:00
parent df9b06843f
commit 8795a4cdeb
13 changed files with 129 additions and 14 deletions

View File

@ -18,7 +18,7 @@ tenant-id: 1
### 请求 /list-menus 接口 => 成功 ### 请求 /list-menus 接口 => 成功
GET {{baseUrl}}/list-menus GET {{baseUrl}}/list-menus
Authorization: Bearer {{token}} Authorization: Bearer {{token}}
#Authorization: Bearer 0d161f69c9ac4c7f836e1b850715a7b0 #Authorization: Bearer a6aa7714a2e44c95aaa8a2c5adc2a67a
tenant-id: 1 tenant-id: 1
### 请求 /druid/xxx 接口 => 失败 TODO 临时测试 ### 请求 /druid/xxx 接口 => 失败 TODO 临时测试

View File

@ -17,13 +17,15 @@ public interface WebFilterOrderEnum {
// OrderedRequestContextFilter 默认为 -105用于国际化上下文等等 // OrderedRequestContextFilter 默认为 -105用于国际化上下文等等
int TENANT_CONTEXT_FILTER = - 100; // 需要保证在 ApiAccessLogFilter 前面 int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面
int API_ACCESS_LOG_FILTER = -90; // 需要保证在 RequestBodyCacheFilter 后面 int API_ACCESS_LOG_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面
int XSS_FILTER = -80; // 需要保证在 RequestBodyCacheFilter 后面 int XSS_FILTER = -102; // 需要保证在 RequestBodyCacheFilter 后面
// Spring Security Filter 默认为 -100可见 SecurityProperties 配置属性类 // Spring Security Filter 默认为 -100可见 org.springframework.boot.autoconfigure.security.SecurityProperties 配置属性类
int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器后
int DEMO_FILTER = Integer.MAX_VALUE; int DEMO_FILTER = Integer.MAX_VALUE;

View File

@ -27,6 +27,11 @@
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-security</artifactId>
</dependency>
<!-- DB 相关 --> <!-- DB 相关 -->
<dependency> <dependency>
<groupId>cn.iocoder.boot</groupId> <groupId>cn.iocoder.boot</groupId>

View File

@ -14,6 +14,15 @@ import java.util.Set;
@Data @Data
public class TenantProperties { public class TenantProperties {
// /**
// * 租户是否开启
// */
// private static final Boolean ENABLE_DEFAULT = true;
//
// /**
// * 是否开启
// */
// private Boolean enable = ENABLE_DEFAULT;
/** /**
* 需要多租户的表 * 需要多租户的表
* *

View File

@ -8,12 +8,14 @@ import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/** /**
* 多租户针对 Job 的自动配置 * 多租户针对 Job 的自动配置
* *
* @author 芋道源码 * @author 芋道源码
*/ */
@Configuration
public class YudaoTenantJobAutoConfiguration { public class YudaoTenantJobAutoConfiguration {
@Bean @Bean

View File

@ -0,0 +1,25 @@
package cn.iocoder.yudao.framework.tenant.config;
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 多租户针对 Web 的自动配置
*
* @author 芋道源码
*/
@Configuration
public class YudaoTenantSecurityAutoConfiguration {
@Bean
public FilterRegistrationBean<TenantSecurityWebFilter> tenantSecurityWebFilter() {
FilterRegistrationBean<TenantSecurityWebFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new TenantSecurityWebFilter());
registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER);
return registrationBean;
}
}

View File

@ -4,12 +4,14 @@ import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter; 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;
import org.springframework.context.annotation.Configuration;
/** /**
* 多租户针对 Web 的自动配置 * 多租户针对 Web 的自动配置
* *
* @author 芋道源码 * @author 芋道源码
*/ */
@Configuration
public class YudaoTenantWebAutoConfiguration { public class YudaoTenantWebAutoConfiguration {
@Bean @Bean

View File

@ -11,10 +11,28 @@ public class TenantContextHolder {
private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>(); private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>();
/**
* 获得租户编号
*
* @return 租户编号
*/
public static Long getTenantId() { public static Long getTenantId() {
return TENANT_ID.get(); return TENANT_ID.get();
} }
/**
* 获得租户编号如果不存在则抛出 NullPointerException 异常
*
* @return 租户编号
*/
public static Long getRequiredTenantId() {
Long tenantId = getTenantId();
if (tenantId == null) {
throw new NullPointerException("TenantContextHolder 不存在租户编号");
}
return tenantId;
}
public static void setTenantId(Long tenantId) { public static void setTenantId(Long tenantId) {
TENANT_ID.set(tenantId); TENANT_ID.set(tenantId);
} }

View File

@ -20,8 +20,7 @@ public class TenantDatabaseInterceptor implements TenantLineHandler {
@Override @Override
public Expression getTenantId() { public Expression getTenantId() {
// TODO 芋艿暂时不考虑获取不到的情况此时会存在 NPE 的报错 return new StringValue(TenantContextHolder.getRequiredTenantId().toString());
return new StringValue(TenantContextHolder.getTenantId().toString());
} }
@Override @Override

View File

@ -40,7 +40,7 @@ public class TenantRedisKeyDefine extends RedisKeyDefine {
@Override @Override
public String formatKey(Object... args) { public String formatKey(Object... args) {
args = ArrayUtil.append(args, TenantContextHolder.getTenantId()); args = ArrayUtil.append(args, TenantContextHolder.getRequiredTenantId());
return super.formatKey(args); return super.formatKey(args);
} }

View File

@ -0,0 +1,50 @@
package cn.iocoder.yudao.framework.tenant.core.security;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import lombok.extern.slf4j.Slf4j;
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;
import java.util.Objects;
/**
* 多租户 Security Web 过滤器
* 校验用户访问的租户是否是其所在的租户避免越权问题
*
* @author 芋道源码
*/
@Slf4j
public class TenantSecurityWebFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
LoginUser user = SecurityFrameworkUtils.getLoginUser();
assert user != null; // shouldNotFilter 已经校验
if (!Objects.equals(user.getTenantId(), TenantContextHolder.getTenantId())) {
log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]",
user.getTenantId(), user.getId(), user.getUserType(),
TenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod());
ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(),
"您无权访问该租户的数据"));
return;
}
// 继续过滤
chain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return SecurityFrameworkUtils.getLoginUser() == null;
}
}

View File

@ -1,15 +1,17 @@
/** /**
* 多租户支持如下层面 * 多租户支持如下层面
* 1. DB基于 MyBatis Plus 多租户的功能实现 * 1. DB基于 MyBatis Plus 多租户的功能实现
* 2. Web请求 HTTP API 解析 Header tenant-id 租户编号添加到租户上下文 * 2. Redis通过在 Redis Key 上拼接租户编号的方式进行隔离
* 3. Job JobHandler 执行任务时会按照每个租户都独立并行执行一次 * 3. Web请求 HTTP API 解析 Header tenant-id 租户编号添加到租户上下文
* 4. MQ Producer 发送消息时Header 带上 tenant-id 租户编号 Consumer 消费消息时 Header tenant-id 租户编号添加到租户上下文 * 4. Security校验当前登陆的用户是否越权访问其它租户的数据
* 5. Async异步需要保证 ThreadLocal 的传递性通过使用阿里开源的 TransmittableThreadLocal 实现相关的改造点可见 * 5. Job JobHandler 执行任务时会按照每个租户都独立并行执行一次
* 6. MQ Producer 发送消息时Header 带上 tenant-id 租户编号 Consumer 消费消息时 Header tenant-id 租户编号添加到租户上下文
* 7. Async异步需要保证 ThreadLocal 的传递性通过使用阿里开源的 TransmittableThreadLocal 实现相关的改造点可见
* 1Spring Async * 1Spring Async
* {@link cn.iocoder.yudao.framework.quartz.config.YudaoAsyncAutoConfiguration#threadPoolTaskExecutorBeanPostProcessor()} * {@link cn.iocoder.yudao.framework.quartz.config.YudaoAsyncAutoConfiguration#threadPoolTaskExecutorBeanPostProcessor()}
* 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

@ -2,4 +2,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.yudao.framework.tenant.config.YudaoTenantDatabaseAutoConfiguration,\ cn.iocoder.yudao.framework.tenant.config.YudaoTenantDatabaseAutoConfiguration,\
cn.iocoder.yudao.framework.tenant.config.YudaoTenantWebAutoConfiguration,\ cn.iocoder.yudao.framework.tenant.config.YudaoTenantWebAutoConfiguration,\
cn.iocoder.yudao.framework.tenant.config.YudaoTenantJobAutoConfiguration,\ cn.iocoder.yudao.framework.tenant.config.YudaoTenantJobAutoConfiguration,\
cn.iocoder.yudao.framework.tenant.config.YudaoTenantMQAutoConfiguration cn.iocoder.yudao.framework.tenant.config.YudaoTenantMQAutoConfiguration,\
cn.iocoder.yudao.framework.tenant.config.YudaoTenantSecurityAutoConfiguration