Reputation: 971
How can we create a multi-tenant application in spring webflux using Mongodb-reactive repository?
I cannot find any complete resources on the web for reactive applications. all the resources available are for non-reactive applications.
UPDATE:
In a non-reactive application, we used to store contextual data in ThreadLocal but this cannot be done with reactive applications as there is thread switching. There is a way to store contextual info in reactor Context inside a WebFilter, But I don't how get hold of that data in ReactiveMongoDatabaseFactory
class.
Thanks.
Upvotes: 4
Views: 2979
Reputation: 829
Here is my very rough working solution for Spring WebFlux - they have since updated the ReactiveMongoDatabaseFactory - getMongoDatabase to return a Mono
Create web filter
public class TenantContextFilter implements WebFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(TenantContextFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange swe, WebFilterChain wfc) {
ServerHttpRequest request = swe.getRequest();
HttpHeaders headers = request.getHeaders();
if(headers.getFirst("X-TENANT-ID") == null){
LOGGER.info(String.format("Missing X-TENANT-ID header"));
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
}
String tenantId = headers.getFirst("X-TENANT-ID");
LOGGER.info(String.format("Processing request with tenant identifier [%s]", tenantId));
return wfc.filter(swe)
.contextWrite(TenantContextHolder.setTenantId(tenantId));
}
}
Create class to get context (credit to somewhere I found this)
public class TenantContextHolder {
public static final String TENANT_ID = TenantContextHolder.class.getName() + ".TENANT_ID";
public static Context setTenantId(String id) {
return Context.of(TENANT_ID, Mono.just(id));
}
public static Mono<String> getTenantId() {
return Mono.deferContextual(contextView -> {
if (contextView.hasKey(TENANT_ID)) {
return contextView.get(TENANT_ID);
}
return Mono.empty();
}
);
}
public static Function<Context, Context> clearContext() {
return (context) -> context.delete(TENANT_ID);
}
}
My spring security setup (all requests allowed for testing)
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain WebFilterChain(ServerHttpSecurity http) {
return http
.formLogin(it -> it.disable())
.cors(it -> it.disable()) //fix this
.httpBasic(it -> it.disable())
.csrf(it -> it.disable())
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
.authorizeExchange(it -> it.anyExchange().permitAll()) //allow anonymous
.addFilterAt(new TenantContextFilter(), SecurityWebFiltersOrder.HTTP_BASIC)
.build();
}
}
Create Tenant Mongo DB Factory
I still have some clean-up work for defaults etc...
public class MultiTenantMongoDBFactory extends SimpleReactiveMongoDatabaseFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(MultiTenantMongoDBFactory.class);
private final String defaultDb;
public MultiTenantMongoDBFactory(MongoClient mongoClient, String databaseName) {
super(mongoClient, databaseName);
this.defaultDb = databaseName;
}
@Override
public Mono<MongoDatabase> getMongoDatabase() throws DataAccessException {
return TenantContextHolder.getTenantId()
.map(id -> {
LOGGER.info(String.format("Database trying to retrieved is [%s]", id));
return super.getMongoDatabase(id);
})
.flatMap(db -> {
return db;
})
.log();
}
}
Configuration Class
@Configuration
@EnableReactiveMongoAuditing
@EnableReactiveMongoRepositories(basePackages = {"com.order.repository"})
class MongoDbConfiguration {
@Bean
public ReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory() {
return new MultiTenantMongoDBFactory(MongoClients.create("mongodb://user:password@localhost:27017"), "tenant_catalog");
}
@Bean
public ReactiveMongoTemplate reactiveMongoTemplate() {
ReactiveMongoTemplate template = new ReactiveMongoTemplate(reactiveMongoDatabaseFactory());
template.setWriteResultChecking(WriteResultChecking.EXCEPTION);
return template;
}
}
Entity Class
@Document(collection = "order")
//getters
//setters
Testing
Create two mongo db's with same collection, put different documents in both
In Postman I just did a get request with the "X-TENANT-ID" header and database name as the value (e.g. tenant-12343 or tenant-34383) and good to go!
Upvotes: 1
Reputation: 971
I was able to Implement Multi-Tenancy in Spring Reactive application using mangodb. Main classes responsible for realizing were: Custom MongoDbFactory class, WebFilter class (instead of Servlet Filter) for capturing tenant info and a ThreadLocal class for storing tenant info. Flow is very simple:
getMongoDatabase()
method to return database based on current tenant available in ThreadLocal class.Source code is:
CurrentTenantHolder.java
package com.jazasoft.demo;
public class CurrentTenantHolder {
private static final ThreadLocal<String> currentTenant = new InheritableThreadLocal<>();
public static String get() {
return currentTenant.get();
}
public static void set(String tenant) {
currentTenant.set(tenant);
}
public static String remove() {
synchronized (currentTenant) {
String tenant = currentTenant.get();
currentTenant.remove();
return tenant;
}
}
}
TenantContextWebFilter.java
package com.example.demo;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
@Component
public class TenantContextWebFilter implements WebFilter {
public static final String TENANT_HTTP_HEADER = "X-Tenant";
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
if (request.getHeaders().containsKey(TENANT_HTTP_HEADER)) {
String tenant = request.getHeaders().getFirst(TENANT_HTTP_HEADER);
CurrentTenantHolder.set(tenant);
}
return chain.filter(exchange).doOnSuccessOrError((Void v, Throwable throwable) -> CurrentTenantHolder.remove());
}
}
MultiTenantMongoDbFactory.java
package com.example.demo;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoDatabase;
import org.springframework.dao.DataAccessException;
import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory;
public class MultiTenantMongoDbFactory extends SimpleReactiveMongoDatabaseFactory {
private final String defaultDatabase;
public MultiTenantMongoDbFactory(MongoClient mongoClient, String databaseName) {
super(mongoClient, databaseName);
this.defaultDatabase = databaseName;
}
@Override
public MongoDatabase getMongoDatabase() throws DataAccessException {
final String tlName = CurrentTenantHolder.get();
final String dbToUse = (tlName != null ? tlName : this.defaultDatabase);
return super.getMongoDatabase(dbToUse);
}
}
MongoDbConfig.java
package com.example.demo;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.ReactiveMongoClientFactoryBean;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
@Configuration
public class MongoDbConfig {
@Bean
public ReactiveMongoTemplate reactiveMongoTemplate(MultiTenantMongoDbFactory multiTenantMongoDbFactory) {
return new ReactiveMongoTemplate(multiTenantMongoDbFactory);
}
@Bean
public MultiTenantMongoDbFactory multiTenantMangoDbFactory(MongoClient mongoClient) {
return new MultiTenantMongoDbFactory(mongoClient, "test1");
}
@Bean
public ReactiveMongoClientFactoryBean mongoClient() {
ReactiveMongoClientFactoryBean clientFactory = new ReactiveMongoClientFactoryBean();
clientFactory.setHost("localhost");
return clientFactory;
}
}
UPDATE:
In reactive-stream we cannot store contextual information in ThreadLocal any more as the request is not tied to a single thread, So, This is not the correct solution.
However, Contextual information can be stored reactor Context in WebFilter like this. chain.filter(exchange).subscriberContext(context -> context.put("tenant", tenant));
. Problem is how do get hold of this contextual info in ReactiveMongoDatabaseFactory
implementation class.
Upvotes: 1