From 9d2de1faafb4feb1bd44e0b72c12cf6f44fc84e5 Mon Sep 17 00:00:00 2001 From: Zuev Date: Fri, 13 Mar 2026 02:36:01 +0300 Subject: [PATCH] feat: Configure and route multi-tenant data sources using an interceptor. --- .../config/tenant/TenantDataSourceConfig.java | 6 ++- .../app/config/tenant/TenantInterceptor.java | 44 ++++++++++++++++++- .../tenant/TenantRoutingDataSource.java | 20 +++++---- 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/backend/src/main/java/com/magistr/app/config/tenant/TenantDataSourceConfig.java b/backend/src/main/java/com/magistr/app/config/tenant/TenantDataSourceConfig.java index a2eb55d..045c255 100755 --- a/backend/src/main/java/com/magistr/app/config/tenant/TenantDataSourceConfig.java +++ b/backend/src/main/java/com/magistr/app/config/tenant/TenantDataSourceConfig.java @@ -120,8 +120,10 @@ public class TenantDataSourceConfig implements WebMvcConfigurer { } @Bean - public TenantInterceptor tenantInterceptor() { - return new TenantInterceptor(); + public TenantInterceptor tenantInterceptor(TenantRoutingDataSource routingDataSource) { + TenantInterceptor interceptor = new TenantInterceptor(); + interceptor.setRoutingDataSource(routingDataSource); + return interceptor; } @Override diff --git a/backend/src/main/java/com/magistr/app/config/tenant/TenantInterceptor.java b/backend/src/main/java/com/magistr/app/config/tenant/TenantInterceptor.java index 9d88f2a..924222b 100755 --- a/backend/src/main/java/com/magistr/app/config/tenant/TenantInterceptor.java +++ b/backend/src/main/java/com/magistr/app/config/tenant/TenantInterceptor.java @@ -1,28 +1,70 @@ package com.magistr.app.config.tenant; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.servlet.HandlerInterceptor; +import java.io.IOException; +import java.util.Map; + /** * Interceptor: извлекает поддомен из Host header и кладёт в TenantContext. * + * Если тенант не настроен в TenantRoutingDataSource — + * сразу возвращает HTTP 404 (не допускает fallback на чужой тенант). + * * Примеры: * "swsu.zuev.company" → tenant = "swsu" * "mgu.zuev.company" → tenant = "mgu" * "localhost" → tenant = "default" * "localhost:8080" → tenant = "default" + * + * API управления тенантами (/api/database/**) пропускается без проверки, + * чтобы администратор мог добавлять тенантов с любого домена. */ public class TenantInterceptor implements HandlerInterceptor { private static final Logger log = LoggerFactory.getLogger(TenantInterceptor.class); + private TenantRoutingDataSource routingDataSource; + + /** + * Устанавливается после создания бина (из TenantDataSourceConfig). + */ + public void setRoutingDataSource(TenantRoutingDataSource routingDataSource) { + this.routingDataSource = routingDataSource; + } + @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { String host = request.getHeader("Host"); String tenant = resolveTenant(host); + String path = request.getRequestURI(); + + // API управления тенантами — всегда пропускаем + // (нужно чтобы админ мог добавить тенант даже если его домен не настроен) + if (path.startsWith("/api/database")) { + TenantContext.setCurrentTenant(tenant); + log.debug("Database API request, tenant '{}' (no strict check)", tenant); + return true; + } + + // Проверяем, существует ли тенант + if (routingDataSource != null && !routingDataSource.hasTenant(tenant)) { + log.warn("Unknown tenant '{}' from Host '{}' — returning 404", tenant, host); + response.setStatus(404); + response.setContentType("application/json;charset=UTF-8"); + new ObjectMapper().writeValue(response.getOutputStream(), Map.of( + "error", "Тенант не найден", + "tenant", tenant, + "message", "Домен " + host + " не настроен. Обратитесь к администратору." + )); + return false; // Останавливаем обработку запроса + } + TenantContext.setCurrentTenant(tenant); log.debug("Resolved tenant '{}' from Host '{}'", tenant, host); return true; diff --git a/backend/src/main/java/com/magistr/app/config/tenant/TenantRoutingDataSource.java b/backend/src/main/java/com/magistr/app/config/tenant/TenantRoutingDataSource.java index e7e9e87..ea152b0 100755 --- a/backend/src/main/java/com/magistr/app/config/tenant/TenantRoutingDataSource.java +++ b/backend/src/main/java/com/magistr/app/config/tenant/TenantRoutingDataSource.java @@ -33,16 +33,18 @@ public class TenantRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { String tenant = TenantContext.getCurrentTenant(); - if (tenant != null && dataSources.containsKey(tenant)) { - return tenant; + + if (tenant == null) { + // Нет HTTP контекста (JPA init, background tasks) — берём первый доступный + if (!dataSources.isEmpty()) { + return dataSources.keySet().iterator().next().toString(); + } + return "default"; } - // Fallback: если нет контекста (например при инициализации JPA) - // или тенант не найден — берём первый доступный - if (!dataSources.isEmpty()) { - String fallback = dataSources.keySet().iterator().next().toString(); - return fallback; - } - return "default"; + + // HTTP запрос — возвращаем точный ключ тенанта + // Если тенанта нет — TenantInterceptor уже вернул 404 + return tenant; } /**