feat: Configure and route multi-tenant data sources using an interceptor.
Some checks failed
Build and Push Docker Images / build-and-push-backend (push) Failing after 22s
Build and Push Docker Images / build-and-push-frontend (push) Successful in 11s
Build and Push Docker Images / deploy-to-k8s (push) Has been skipped

This commit is contained in:
Zuev
2026-03-13 02:36:01 +03:00
parent 59caa9d6cc
commit 9d2de1faaf
3 changed files with 58 additions and 12 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
}
/**