feat: Configure and route multi-tenant data sources using an interceptor.
This commit is contained in:
@@ -120,8 +120,10 @@ public class TenantDataSourceConfig implements WebMvcConfigurer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public TenantInterceptor tenantInterceptor() {
|
public TenantInterceptor tenantInterceptor(TenantRoutingDataSource routingDataSource) {
|
||||||
return new TenantInterceptor();
|
TenantInterceptor interceptor = new TenantInterceptor();
|
||||||
|
interceptor.setRoutingDataSource(routingDataSource);
|
||||||
|
return interceptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,28 +1,70 @@
|
|||||||
package com.magistr.app.config.tenant;
|
package com.magistr.app.config.tenant;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interceptor: извлекает поддомен из Host header и кладёт в TenantContext.
|
* Interceptor: извлекает поддомен из Host header и кладёт в TenantContext.
|
||||||
*
|
*
|
||||||
|
* Если тенант не настроен в TenantRoutingDataSource —
|
||||||
|
* сразу возвращает HTTP 404 (не допускает fallback на чужой тенант).
|
||||||
|
*
|
||||||
* Примеры:
|
* Примеры:
|
||||||
* "swsu.zuev.company" → tenant = "swsu"
|
* "swsu.zuev.company" → tenant = "swsu"
|
||||||
* "mgu.zuev.company" → tenant = "mgu"
|
* "mgu.zuev.company" → tenant = "mgu"
|
||||||
* "localhost" → tenant = "default"
|
* "localhost" → tenant = "default"
|
||||||
* "localhost:8080" → tenant = "default"
|
* "localhost:8080" → tenant = "default"
|
||||||
|
*
|
||||||
|
* API управления тенантами (/api/database/**) пропускается без проверки,
|
||||||
|
* чтобы администратор мог добавлять тенантов с любого домена.
|
||||||
*/
|
*/
|
||||||
public class TenantInterceptor implements HandlerInterceptor {
|
public class TenantInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(TenantInterceptor.class);
|
private static final Logger log = LoggerFactory.getLogger(TenantInterceptor.class);
|
||||||
|
|
||||||
|
private TenantRoutingDataSource routingDataSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Устанавливается после создания бина (из TenantDataSourceConfig).
|
||||||
|
*/
|
||||||
|
public void setRoutingDataSource(TenantRoutingDataSource routingDataSource) {
|
||||||
|
this.routingDataSource = routingDataSource;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@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 host = request.getHeader("Host");
|
||||||
String tenant = resolveTenant(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);
|
TenantContext.setCurrentTenant(tenant);
|
||||||
log.debug("Resolved tenant '{}' from Host '{}'", tenant, host);
|
log.debug("Resolved tenant '{}' from Host '{}'", tenant, host);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -33,18 +33,20 @@ public class TenantRoutingDataSource extends AbstractRoutingDataSource {
|
|||||||
@Override
|
@Override
|
||||||
protected Object determineCurrentLookupKey() {
|
protected Object determineCurrentLookupKey() {
|
||||||
String tenant = TenantContext.getCurrentTenant();
|
String tenant = TenantContext.getCurrentTenant();
|
||||||
if (tenant != null && dataSources.containsKey(tenant)) {
|
|
||||||
return tenant;
|
if (tenant == null) {
|
||||||
}
|
// Нет HTTP контекста (JPA init, background tasks) — берём первый доступный
|
||||||
// Fallback: если нет контекста (например при инициализации JPA)
|
|
||||||
// или тенант не найден — берём первый доступный
|
|
||||||
if (!dataSources.isEmpty()) {
|
if (!dataSources.isEmpty()) {
|
||||||
String fallback = dataSources.keySet().iterator().next().toString();
|
return dataSources.keySet().iterator().next().toString();
|
||||||
return fallback;
|
|
||||||
}
|
}
|
||||||
return "default";
|
return "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTP запрос — возвращаем точный ключ тенанта
|
||||||
|
// Если тенанта нет — TenantInterceptor уже вернул 404
|
||||||
|
return tenant;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Добавляет тенант и создаёт для него HikariCP пул.
|
* Добавляет тенант и создаёт для него HikariCP пул.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user