I'm working on a SaaS project using Spring WebFlux and Reactive MongoDB. It needs to be a MultiTenant application and each tenant must use its own database.
As for now I just added the Reactive MongoDB dependency to the pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
Then I extended the AbstractReactiveMongoConfiguration in order to provide the MongoClient and the DatabaseName:
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoClients;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.AbstractReactiveMongoConfiguration;
@Configuration
public class DatabaseConfiguration extends AbstractReactiveMongoConfiguration {
@Override
public MongoClient reactiveMongoClient() {
System.out.println("ReactiveMongoClient");
return MongoClients.create();
}
@Override
protected String getDatabaseName() {
System.out.println("DataBase name");
return "gateway";
}
}
The whole application is an OAuth 2.0 Resource Server and I'm able to retrieve the TenantID from the Authentication in the ReactiveSecurityContextHolder.
public Mono<String> tenantID() {
return ReactiveSecurityContextHolder.getContext()
.switchIfEmpty(Mono.empty())
.flatMap((securityContext) -> {
Authentication authentication = securityContext.getAuthentication();
if (authentication instanceof CustomAuthenticationToken) {
CustomAuthenticationToken customAuthenticationToken = (customAuthenticationToken) authentication;
Jwt jwt = customAuthenticationToken.getToken();
String issuer = jwt.getIssuer().toString();
return Mono.justOrEmpty(issuer);
}
return Mono.empty();
});
}
What is the next step in order to switch the database based on the user (Authentication) performing the request?
UPDATE:
This is almost near to the goal, but almost one year ago @mp911de said it was not possible. Wondering if it is feasible now. How could I implement a true reactive ReactiveMongoDatabaseFactory that returns Mono so that I could access SecurityContext, hence the Authentication, before returning the MongoDatabase?
I had the same problem a few months ago and wanted to share how I solved it.
Spring does not provide any out-of-the-box solution for this problem. A few months ago i created a proof of concept how to solve it and just published the example project on Github.
spring-mongodb-multi-tenancy-example
In short: I created a custom MultiTenancyReactiveMongoTemplate
which internally delegates to the the actual ReactiveMongoTemplate
based on the tenantId from subscriberContext. The tenantId is extracted from a http-request header in a custom WebFilter which puts it in the subscriberContext.
This workaround works for most cases and also supports auto-index creation and the use of the ReactiveMongoRepository
's.
But also has some limitations as Transactions, IndexOps on the MultiTenancyReactiveMongoTemplate
or the ReactiveGridFSTemplate
do not work with the provided solution. Some of the things could be implemented with other delegating 'templates' but some things will never work as these operations simply return scalar values (no Publisher
) and there is no way to access the subscriberContext in these cases.
If you do not need these features you could probably go with this solution.
You spin up and configure instances of the service for each tenant/customer and put a reverse proxy 'before' these services. The reverse proxy decides which service should be used to handle the request. The reverse proxy can be implemented very easy with for example Spring Cloud Gateway which allows you to easily implement predicates which decide (e. g. based on a auth token) which service should be used.
With technologies like CloudFoundry or Kubernetes it is no that hard anymore to orchestrate the deployment of these tenant specific services and this solution makes it also easy to do tenant specific monitoring, alerting or even billing.
We chose solution 2 because overall this was easier and scales better for us.