|
@@ -3,6 +3,7 @@ package com.fs.core.filter;
|
|
|
import com.alibaba.fastjson.JSONObject;
|
|
import com.alibaba.fastjson.JSONObject;
|
|
|
import com.fs.app.utils.JwtUtils;
|
|
import com.fs.app.utils.JwtUtils;
|
|
|
import com.fs.common.enums.DataSourceType;
|
|
import com.fs.common.enums.DataSourceType;
|
|
|
|
|
+import com.fs.common.utils.StringUtils;
|
|
|
import com.fs.config.saas.ProjectConfig;
|
|
import com.fs.config.saas.ProjectConfig;
|
|
|
import com.fs.core.config.TenantConfigContext;
|
|
import com.fs.core.config.TenantConfigContext;
|
|
|
import com.fs.core.datasource.DynamicDataSourceContextHolder;
|
|
import com.fs.core.datasource.DynamicDataSourceContextHolder;
|
|
@@ -14,7 +15,6 @@ import com.fs.tenant.domain.TenantInfo;
|
|
|
import com.fs.tenant.service.TenantInfoService;
|
|
import com.fs.tenant.service.TenantInfoService;
|
|
|
import io.jsonwebtoken.Claims;
|
|
import io.jsonwebtoken.Claims;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
-import org.apache.commons.lang3.StringUtils;
|
|
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.core.annotation.Order;
|
|
import org.springframework.core.annotation.Order;
|
|
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
@@ -45,17 +45,20 @@ public class CompanySaasContextFilter extends OncePerRequestFilter {
|
|
|
private TenantDataSourceManager tenantDataSourceManager;
|
|
private TenantDataSourceManager tenantDataSourceManager;
|
|
|
@Resource
|
|
@Resource
|
|
|
private SysConfigMapper sysConfigMapper;
|
|
private SysConfigMapper sysConfigMapper;
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 租户编码请求头名称。
|
|
|
|
|
+ * 前端需在每次请求中携带该头,才能启用多租户能力。
|
|
|
|
|
+ */
|
|
|
|
|
+ public static final String HEADER_TENANT_CODE = "X-Tenant-Code";
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
|
protected void doFilterInternal(HttpServletRequest request,
|
|
protected void doFilterInternal(HttpServletRequest request,
|
|
|
HttpServletResponse response,
|
|
HttpServletResponse response,
|
|
|
FilterChain chain) throws ServletException, IOException {
|
|
FilterChain chain) throws ServletException, IOException {
|
|
|
|
|
+ String tenantCode = request.getHeader(HEADER_TENANT_CODE);
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
- String loginPath = "/app/user/login";
|
|
|
|
|
- AntPathMatcher pathMatcher = new AntPathMatcher();
|
|
|
|
|
- String requestPath = request.getRequestURI();
|
|
|
|
|
- // 登录接口,直接放行,不执行后续租户逻辑
|
|
|
|
|
- if (pathMatcher.match(loginPath, requestPath)) {
|
|
|
|
|
|
|
+ if (StringUtils.isBlank(tenantCode)) {
|
|
|
SysConfig cfg = sysConfigMapper.selectConfigByConfigKey("projectConfig");
|
|
SysConfig cfg = sysConfigMapper.selectConfigByConfigKey("projectConfig");
|
|
|
if (cfg != null && com.fs.common.utils.StringUtils.isNotBlank(cfg.getConfigValue())) {
|
|
if (cfg != null && com.fs.common.utils.StringUtils.isNotBlank(cfg.getConfigValue())) {
|
|
|
TenantConfigContext.set(JSONObject.parseObject(cfg.getConfigValue()));
|
|
TenantConfigContext.set(JSONObject.parseObject(cfg.getConfigValue()));
|
|
@@ -64,48 +67,40 @@ public class CompanySaasContextFilter extends OncePerRequestFilter {
|
|
|
}
|
|
}
|
|
|
ProjectConfig.loadTenantConfigsFromContext();
|
|
ProjectConfig.loadTenantConfigsFromContext();
|
|
|
|
|
|
|
|
|
|
+ // 未携带租户编码:保持单库行为,直接放行
|
|
|
chain.doFilter(request, response);
|
|
chain.doFilter(request, response);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- String token = request.getHeader(jwtUtils.getHeader());
|
|
|
|
|
|
|
+ // Step 1:切到主库,根据 tenantCode 查询租户信息
|
|
|
|
|
+ DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
|
|
|
|
|
|
|
|
- Long tenantId = null;
|
|
|
|
|
- if (StringUtils.isNotBlank(token)) {
|
|
|
|
|
- Claims claims = jwtUtils.getClaimByToken(token);
|
|
|
|
|
- if (claims != null && !jwtUtils.isTokenExpired(claims.getExpiration())) {
|
|
|
|
|
- tenantId = jwtUtils.getTenantId(claims);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- // 2) 不做租户切换
|
|
|
|
|
- if (tenantId == null) {
|
|
|
|
|
|
|
+ TenantInfo query = new TenantInfo();
|
|
|
|
|
+ query.setTenantCode(tenantCode);
|
|
|
|
|
+ List<TenantInfo> tenants = tenantInfoService.selectTenantInfoList(query);
|
|
|
|
|
+ if (tenants == null || tenants.isEmpty()) {
|
|
|
|
|
+ log.warn("[SaaS user-app] 请求携带的 tenantCode={} 未找到租户记录,按单库模式处理。", tenantCode);
|
|
|
chain.doFilter(request, response);
|
|
chain.doFilter(request, response);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- // 3) 切到主库查 tenant_info(拿到租户库连接信息)
|
|
|
|
|
- DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
|
|
|
|
|
- TenantInfo tenant = null;
|
|
|
|
|
- if (tenantId != null) {
|
|
|
|
|
- tenant = tenantInfoService.selectTenantInfoById(String.valueOf(tenantId));
|
|
|
|
|
- if (tenant == null) {
|
|
|
|
|
- log.warn("[SaaS company-app] tenantId={} 未找到租户", tenantId);
|
|
|
|
|
- chain.doFilter(request, response);
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- // 4) 基础校验
|
|
|
|
|
|
|
+
|
|
|
|
|
+ TenantInfo tenant = tenants.get(0);
|
|
|
|
|
+
|
|
|
|
|
+ // 基础校验:启用状态 + 未过期
|
|
|
if (!Integer.valueOf(1).equals(tenant.getStatus())) {
|
|
if (!Integer.valueOf(1).equals(tenant.getStatus())) {
|
|
|
- log.warn("[SaaS company-app] tenantId={}, tenantCode={} 已禁用", tenant.getId(), tenant.getTenantCode());
|
|
|
|
|
|
|
+ log.warn("[SaaS user-app] 租户 tenantCode={} 已禁用,按单库模式处理。", tenantCode);
|
|
|
chain.doFilter(request, response);
|
|
chain.doFilter(request, response);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
if (tenant.getExpireTime() != null && tenant.getExpireTime().before(new Date())) {
|
|
if (tenant.getExpireTime() != null && tenant.getExpireTime().before(new Date())) {
|
|
|
- log.warn("[SaaS company-app] tenantId={}, tenantCode={} 已过期", tenant.getId(), tenant.getTenantCode());
|
|
|
|
|
|
|
+ log.warn("[SaaS user-app] 租户 tenantCode={} 已过期,按单库模式处理。", tenantCode);
|
|
|
chain.doFilter(request, response);
|
|
chain.doFilter(request, response);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- // 5) 切租户库
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Step 2:切换到租户库(如无则动态创建数据源)
|
|
|
tenantDataSourceManager.switchTenant(tenant);
|
|
tenantDataSourceManager.switchTenant(tenant);
|
|
|
- // 6) 加载租户 projectConfig -> TenantConfigContext
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Step 3:从租户库读取项目配置(projectConfig),填充 TenantConfigContext 和 ProjectConfig
|
|
|
SysConfig cfg = sysConfigMapper.selectConfigByConfigKey("projectConfig");
|
|
SysConfig cfg = sysConfigMapper.selectConfigByConfigKey("projectConfig");
|
|
|
if (cfg != null && StringUtils.isNotBlank(cfg.getConfigValue())) {
|
|
if (cfg != null && StringUtils.isNotBlank(cfg.getConfigValue())) {
|
|
|
TenantConfigContext.set(JSONObject.parseObject(cfg.getConfigValue()));
|
|
TenantConfigContext.set(JSONObject.parseObject(cfg.getConfigValue()));
|
|
@@ -113,14 +108,15 @@ public class CompanySaasContextFilter extends OncePerRequestFilter {
|
|
|
TenantConfigContext.set(null);
|
|
TenantConfigContext.set(null);
|
|
|
}
|
|
}
|
|
|
ProjectConfig.loadTenantConfigsFromContext();
|
|
ProjectConfig.loadTenantConfigsFromContext();
|
|
|
- // 8) 设置 SecurityContext(Redis Key 自动租户前缀依赖这里)
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 设置租户到 SecurityContext,供 TenantKeyRedisSerializer 自动为 Redis Key 加 tenantid 前缀
|
|
|
SecurityContextHolder.getContext().setAuthentication(
|
|
SecurityContextHolder.getContext().setAuthentication(
|
|
|
new UsernamePasswordAuthenticationToken(
|
|
new UsernamePasswordAuthenticationToken(
|
|
|
new TenantPrincipal(tenant.getId()),
|
|
new TenantPrincipal(tenant.getId()),
|
|
|
null,
|
|
null,
|
|
|
- Collections.emptyList()
|
|
|
|
|
- )
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ Collections.emptyList()));
|
|
|
|
|
+
|
|
|
|
|
+ // Step 4:继续后续过滤器链和业务处理(此时 Mapper 操作的就是当前租户库)
|
|
|
chain.doFilter(request, response);
|
|
chain.doFilter(request, response);
|
|
|
} finally {
|
|
} finally {
|
|
|
try {
|
|
try {
|