mirror of
https://gitee.com/huangge1199_admin/vue-pro.git
synced 2024-11-22 15:21:53 +08:00
增加 Tenant Security 的实现
This commit is contained in:
parent
df9b06843f
commit
8795a4cdeb
@ -18,7 +18,7 @@ tenant-id: 1
|
||||
### 请求 /list-menus 接口 => 成功
|
||||
GET {{baseUrl}}/list-menus
|
||||
Authorization: Bearer {{token}}
|
||||
#Authorization: Bearer 0d161f69c9ac4c7f836e1b850715a7b0
|
||||
#Authorization: Bearer a6aa7714a2e44c95aaa8a2c5adc2a67a
|
||||
tenant-id: 1
|
||||
|
||||
### 请求 /druid/xxx 接口 => 失败 TODO 临时测试
|
||||
|
@ -17,13 +17,15 @@ public interface WebFilterOrderEnum {
|
||||
|
||||
// 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;
|
||||
|
||||
|
@ -27,6 +27,11 @@
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- DB 相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
|
@ -14,6 +14,15 @@ import java.util.Set;
|
||||
@Data
|
||||
public class TenantProperties {
|
||||
|
||||
// /**
|
||||
// * 租户是否开启
|
||||
// */
|
||||
// private static final Boolean ENABLE_DEFAULT = true;
|
||||
//
|
||||
// /**
|
||||
// * 是否开启
|
||||
// */
|
||||
// private Boolean enable = ENABLE_DEFAULT;
|
||||
/**
|
||||
* 需要多租户的表
|
||||
*
|
||||
|
@ -8,12 +8,14 @@ import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 多租户针对 Job 的自动配置
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration
|
||||
public class YudaoTenantJobAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -4,12 +4,14 @@ import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
|
||||
import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 多租户针对 Web 的自动配置
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration
|
||||
public class YudaoTenantWebAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
|
@ -11,10 +11,28 @@ public class TenantContextHolder {
|
||||
|
||||
private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 获得租户编号。
|
||||
*
|
||||
* @return 租户编号
|
||||
*/
|
||||
public static Long getTenantId() {
|
||||
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) {
|
||||
TENANT_ID.set(tenantId);
|
||||
}
|
||||
|
@ -20,8 +20,7 @@ public class TenantDatabaseInterceptor implements TenantLineHandler {
|
||||
|
||||
@Override
|
||||
public Expression getTenantId() {
|
||||
// TODO 芋艿:暂时不考虑获取不到的情况。此时,会存在 NPE 的报错
|
||||
return new StringValue(TenantContextHolder.getTenantId().toString());
|
||||
return new StringValue(TenantContextHolder.getRequiredTenantId().toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -40,7 +40,7 @@ public class TenantRedisKeyDefine extends RedisKeyDefine {
|
||||
|
||||
@Override
|
||||
public String formatKey(Object... args) {
|
||||
args = ArrayUtil.append(args, TenantContextHolder.getTenantId());
|
||||
args = ArrayUtil.append(args, TenantContextHolder.getRequiredTenantId());
|
||||
return super.formatKey(args);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -1,15 +1,17 @@
|
||||
/**
|
||||
* 多租户,支持如下层面:
|
||||
* 1. DB:基于 MyBatis Plus 多租户的功能实现。
|
||||
* 2. Web:请求 HTTP API 时,解析 Header 的 tenant-id 租户编号,添加到租户上下文。
|
||||
* 3. Job:在 JobHandler 执行任务时,会按照每个租户,都独立并行执行一次。
|
||||
* 4. MQ:在 Producer 发送消息时,Header 带上 tenant-id 租户编号;在 Consumer 消费消息时,将 Header 的 tenant-id 租户编号,添加到租户上下文。
|
||||
* 5. Async:异步需要保证 ThreadLocal 的传递性,通过使用阿里开源的 TransmittableThreadLocal 实现。相关的改造点,可见:
|
||||
* 2. Redis:通过在 Redis Key 上拼接租户编号的方式,进行隔离。
|
||||
* 3. Web:请求 HTTP API 时,解析 Header 的 tenant-id 租户编号,添加到租户上下文。
|
||||
* 4. Security:校验当前登陆的用户,是否越权访问其它租户的数据。
|
||||
* 5. Job:在 JobHandler 执行任务时,会按照每个租户,都独立并行执行一次。
|
||||
* 6. MQ:在 Producer 发送消息时,Header 带上 tenant-id 租户编号;在 Consumer 消费消息时,将 Header 的 tenant-id 租户编号,添加到租户上下文。
|
||||
* 7. Async:异步需要保证 ThreadLocal 的传递性,通过使用阿里开源的 TransmittableThreadLocal 实现。相关的改造点,可见:
|
||||
* 1)Spring Async:
|
||||
* {@link cn.iocoder.yudao.framework.quartz.config.YudaoAsyncAutoConfiguration#threadPoolTaskExecutorBeanPostProcessor()}
|
||||
* 2)Spring Security:
|
||||
* TransmittableThreadLocalSecurityContextHolderStrategy
|
||||
* 和 YudaoSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法
|
||||
* 6. Redis:通过在 Redis Key 上拼接租户编号的方式,进行隔离。
|
||||
*
|
||||
*/
|
||||
package cn.iocoder.yudao.framework.tenant;
|
||||
|
@ -2,4 +2,5 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
cn.iocoder.yudao.framework.tenant.config.YudaoTenantDatabaseAutoConfiguration,\
|
||||
cn.iocoder.yudao.framework.tenant.config.YudaoTenantWebAutoConfiguration,\
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user