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
|
||||
public TenantInterceptor tenantInterceptor() {
|
||||
return new TenantInterceptor();
|
||||
public TenantInterceptor tenantInterceptor(TenantRoutingDataSource routingDataSource) {
|
||||
TenantInterceptor interceptor = new TenantInterceptor();
|
||||
interceptor.setRoutingDataSource(routingDataSource);
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user