Software as a Service or SaaS has been around for quite some time now. But most of the time, developers are building single tenant applications as per requirements. These applications have just one database and one web server at their core.
Many e-commerce application want a solution where multi-tenancy is achieved by having a database per tenant and all user information (username, password, etc) for authentication and authorization stored in a user table in the respective tenant databases. It meant that not only do they need a multi-tenant application, but also a secure application like any other web application secured by Spring Security.
Let’s see how to use Spring Security to secure a web application and how to use Hibernate to connect to a database. The requirement further dictates that all users belonging to a tenant be stored in the tenant database and not a separate or central database. This would allow for complete data isolation for each tenant.
For this example we are going to built a SaaS style web app using the latest Spring Boot 2, Spring JPA with Hibernate as the JPA provider to connect to MySQL and Spring Security 5 to secure the web application.
A multi-tenant application is where a tenant (i.e. users in a company) feels that the application has been created and deployed for them. In reality, there are many such tenants/business users and they too are using the same application but they get a feel that it is built just for them. Typical examples are online e-commerce marketplaces, storefronts, white labelled shopping mobile apps etc.
SaaS applications are multi-tenant applications which require tenant data isolation in the database layer. There are different approaches for achieving this data isolation.
The most widely used approaches are
There are many factors to consider before choosing a data isolation approach as mentioned above. We will not go into depth of each but focus mainly on building a multi-tenant application using the latest Spring Boot 2 framework along with JPA, Hibernate as the JPA provider and Spring Security 5.
In many applications, especially in financial and medical domains, there is a strict need for absolute data isolation, security and privacy in multi-tenant applications. Multi-tenancy using database per tenant one of the best ways for achieving data isolation and security.
We will build a database per tenant multi-tenant application secured by Spring Security.
In most SaaS applications, there is an entry point where a user belonging to a tenant enters the
Step 1 – Process the login form with the extra ‘tenant’ field
Step 2 – Compare the login form values with existing user in the database
Step 3 – Select the correct tenant database
How does Step 3 work?
The example application was built using:
Let’s take a look at the code for building multi tenancy SaaS application:
This is how a typical project layout will look in Spring STS
Using the Spring Initialzr create a project with Spring Web, Security, Thymeleaf, MySQL and JPA. It should generate a pom.xml as follows:
Note that there are a few extra dependencies added here:
This is the final pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>multitenancy-mysql</artifactId>
<version>1.0.1</version>
<packaging>jar</packaging>
<name>multitenancy-mysql</name>
<description>Spring Boot JPA Hibernate with Per Database Multi-Tenancy with Spring Security</description>
<contributors>
<contributor>
<name>Sunit Katkar</name>
<email>sunitkatkar@gmail.com</email>
<url>https://sunitkatkar.blogspot.com/</url>
</contributor>
</contributors>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
<version>1.9.21</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
I have worked mostly with application.properties files, but found that the YAML format is easier to use when defining multiple data sources. In this example application, I have defined two data sources multitenancy.mtapp.dataSources which are pointing to 2 MySQL databases running on a MySQL instance.
spring:
thymeleaf:
cache: false
mode: LEGACYHTML5
jpa:
database: mysql
show-sql: true
generate-ddl: false
hibernate:
ddl-auto: none
multitenancy:
mtapp:
dataSources:
-
tenantId: tenant_1
url: jdbc:mysql://localhost:3306/dbtenant1?useSSL=false
username: tenant1
password: admin123
driverClassName: com.mysql.jdbc.Driver
-
tenantId: tenant_2
url: jdbc:mysql://localhost:3306/dbtenant2?useSSL=false
username: tenant1
password: admin123
driverClassName: com.mysql.jdbc.Driver
Note that spring.thymeleaf.mode is set to LEGACYHTML5. The Cyber NekoHTML dependency will then take care of not being so strict about parsing HTML5 and ThymeLeaf will not complain.
Note that Spring Security provides the interface UserDetails. An implementation of this interface requires details about the user to be authenticated. For this, Spring Security provides a User class which can be used as is or extended. The example application extends this org.springframework.security.core.userdetails.User class to add the tenant attribute to the User.
package com.example.model;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
/**
* CustomUserDetails class extends the Spring Security provided
* {@link org.springframework.security.core.userdetails.User} class for
* authentication purpose. Do not confuse this with the {@link User} class which
* is an entity for storing application specific user details like username,
* password, tenant, etc in the database using the JPA {@literal @}Entity
* annotation.
*
* @author Sunit Katkar
* @version 1.0
* @since 1.0 (April 2018)
*
*/public class CustomUserDetails
extends org.springframework.security.core.userdetails.User {
private static final long serialVersionUID = 1L;
/**
* The extra field in the login form is for the tenant name
*/ private String tenant;
/**
* Constructor based on the spring security User class but with an extra
* argument <code>tenant</code> to store the tenant name submitted by the end
* user.
*
* @param username
* @param password
* @param authorities
* @param tenant
*/ public CustomUserDetails(String username, String password,
Collection<? extends GrantedAuthority> authorities,
String tenant) {
super(username, password, authorities);
this.tenant = tenant;
}
// Getters and Setters
public String getTenant() {
return tenant;
}
public void setTenant(String tenant) {
this.tenant = tenant;
}
}
Spring Security provides a interface UserDetailsService which has just one method declared in it – loadUserByUsername which returns the UserDetails. Now this UserDetails is used for authentication. In the example application code, the CustomUserDetails already defines the attributes for the User.
package com.example.security;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.example.model.CustomUserDetails;
import com.example.model.Role;
import com.example.model.User;
import com.example.service.UserService;
/**
* {@link CustomUserDetailsService} contract defines a single method called
* loadUserByUsernameAndTenantname.
*
* The {@link CustomUserDetailsServiceImpl} class simply implements the contract
* and delegates to {@link UserService} to get the
* {@link com.example.model.User} from the database so that it can be compared
* with the {@link org.springframework.security.core.userdetails.User} for
* authentication. Authentication occurs via the
* {@link CustomUserDetailsAuthenticationProvider}.
*
* @author Sunit Katkar
* @version 1.0
* @since 1.0 (April 2018)
*
*/@Service("userDetailsService")
public class CustomUserDetailsServiceImpl implements CustomUserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsernameAndTenantname(String username, String tenant)
throws UsernameNotFoundException {
if (StringUtils.isAnyBlank(username, tenant)) {
throw new UsernameNotFoundException("Username and domain must be provided");
}
// Look for the user based on the username and tenant by accessing the
// UserRepository via the UserService
User user = userService.findByUsernameAndTenantname(username, tenant);
if (user == null) {
throw new UsernameNotFoundException(
String.format("Username not found for domain, "
+ "username=%s, tenant=%s", username, tenant));
}
Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
for (Role role : user.getRoles()) {
grantedAuthorities.add(new SimpleGrantedAuthority(role.getRole()));
}
CustomUserDetails customUserDetails =
new CustomUserDetails(user.getUsername(),
user.getPassword(), grantedAuthorities, tenant);
return customUserDetails;
}
}
Spring Security provides class AbstractUserDetailsAuthenticationProvider which allows subclasses to override and work with UserDetails objects. The class is designed to respond to UsernamePasswordAuthenticationToken authentication requests.
The CustomUserDetailsAuthenticationProvider implemented for this application delegates to the CustomUserDetailsService (implemented by CustomUserDetailsServiceImpl) for retrieving the UserDetails for authentication
package com.example.security;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.Assert;
/**
* {@link CustomUserDetailsAuthenticationProvider} extends
* {@link AbstractUserDetailsAuthenticationProvider} and delegates to the
* {@link CustomUserDetailService} to retrieve the User. The most important
* feature of this class is the implementation of the <code>retrieveUser</code>
* method.
*
* Note that the authentication token must be cast to CustomAuthenticationToken
* to access the custom field - tenant
*
*
* @author Sunit Katkar
* @version 1.0
* @since 1.0 (April 2018)
*/public class CustomUserDetailsAuthenticationProvider
extends AbstractUserDetailsAuthenticationProvider {
/**
* The plaintext password used to perform PasswordEncoder#matches(CharSequence,
* String)} on when the user is not found to avoid SEC-2056
* (https://github.com/spring-projects/spring-security/issues/2280).
*/ private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
/**
* For encoding and/or matching the encrypted password stored in the database
* with the user submitted password
*/ private PasswordEncoder passwordEncoder;
private CustomUserDetailsService userDetailsService;
/**
* The password used to perform
* {@link PasswordEncoder#matches(CharSequence, String)} on when the user is not
* found to avoid SEC-2056. This is necessary, because some
* {@link PasswordEncoder} implementations will short circuit if the password is
* not in a valid format.
*/ private String userNotFoundEncodedPassword;
public CustomUserDetailsAuthenticationProvider(PasswordEncoder passwordEncoder,
CustomUserDetailsService userDetailsService) {
this.passwordEncoder = passwordEncoder;
this.userDetailsService = userDetailsService;
}
/*
* (non-Javadoc)
*
* @see org.springframework.security.authentication.dao.
* AbstractUserDetailsAuthenticationProvider#additionalAuthenticationChecks(org.
* springframework.security.core.userdetails.UserDetails,
* org.springframework.security.authentication.
* UsernamePasswordAuthenticationToken)
*/ @Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(
messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
// Get the password submitted by the end user
String presentedPassword = authentication.getCredentials().toString();
// If the password stored in the database and the user submitted password do not
// match, then signal a login error
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(
messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
@Override
protected void doAfterPropertiesSet() throws Exception {
Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
}
/*
* (non-Javadoc)
*
* @see org.springframework.security.authentication.dao.
* AbstractUserDetailsAuthenticationProvider#retrieveUser(java.lang.String,
* org.springframework.security.authentication.
* UsernamePasswordAuthenticationToken)
*/ @Override
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
CustomAuthenticationToken auth = (CustomAuthenticationToken) authentication;
UserDetails loadedUser;
try {
loadedUser = this.userDetailsService
.loadUserByUsernameAndTenantname(auth.getPrincipal().toString(),
auth.getTenant());
} catch (UsernameNotFoundException notFound) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
passwordEncoder.matches(presentedPassword, userNotFoundEncodedPassword);
}
throw notFound;
} catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(repositoryProblem.getMessage(),
repositoryProblem);
}
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, "
+ "which is an interface contract violation");
}
return loadedUser;
}
}
In this part of the blog post, we have seen how this application works, how to set up your project, how Spring Security is set up.
In the next part, I will understand how the JPA and Hibernate part of the code is set up. Also we will see how Spring Security is tied in with the multi-tenancy Hibernate code.
A fun training session on Corporate Etiquette, Customer Service and Team Building at V2STech
We celebrate the work anniversary of our colleagues who have completed over 5 years with…