4

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.

3
  • What are you actually trying to do? Multi-tenancy doesn't have anything to do with the fact the database is reactive, so what part of the process are you stuck on? Commented Apr 22, 2019 at 20:49
  • In a non-reactive scenario using hibernate, I implemented MultiTenantConnectionProvider and CurrentTenantIdentifierResolver provided by hibernate. Here in mangodb, I don't how to achieve the same. Commented Apr 22, 2019 at 21:03
  • Okay so this actually doesn't have anything to do with reactive nature, you're just trying to implement a multi tenancy connection with Mongo. Have a look at this question/answer stackoverflow.com/questions/16325606/… Commented Apr 22, 2019 at 21:42

2 Answers 2

1

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:

  1. Capture Tenant related info from the request in WebFilter and set it in ThreadLocal. Here I am sending Tenant info using header: X-Tenant
  2. Implement Custom MondoDbFactory class and override 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.

Sign up to request clarification or add additional context in comments.

4 Comments

There's no way how to obtain a Context from ReactiveMongoDatabaseFactory because ReactiveMongoDatabaseFactory returns scalar values. ReactiveMongoDatabaseFactory would need to return Mono<MongoDatabase> to be able to access the context.
@mp911de is there any update on this? I would also need to switch the database based on the current tenant that is stored in the ReactiveSecurityContextHolder. stackoverflow.com/questions/60982869/…
@Md Zahid Raza In your last update, you mentioned "Problem is how do get hold of this contextual info in ReactiveMongoDatabaseFactory implementation class". Did you solve that? Can you post the solution? if you solved this
@Md Zahid Raza This post is super useful to understand and solve the problem here described. I highly recommend to check carefully that: Spring Boot WebFlux Reactive MongoDB: how to switch the database on each request?
1

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!

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.