Md Zahid Raza
Md Zahid Raza

Reputation: 971

Multi-Tenancy in Reactive Spring boot application using mongodb-reactive

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

Answers (2)

user521990
user521990

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

Md Zahid Raza
Md Zahid Raza

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:

  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.

Upvotes: 1

Related Questions