Bael 5407: Multitenancy with Spring Data JPA (#12507)
* BAEL-5407: add entity, repository, controller, main class * BAEL-5407: add starter-web and postgres dependencies * BAEL-5407: add sample script * BAEL-5407: add tenant definition file * BAEL-5407: add tenant filter * BAEL-5407: add AbstractRoutingDataSource class * BAEL-5407: add TenantContext class * BAEL-5407: add MultitenantConfiguration class * BAEL-5407: replace Object with String * BAEL-5407: improve indentation * BAEL-5407: remove printStackTrace() * BAEL-5407: add security classes * BAEL-5407: add security dependencies * BAEL-5407: get Tenant from JWT token * BAEL-5407: improve indent * BAEL-5407: improve code quality Co-authored-by: h_sharifi <h_sharifi@modernisc.com>
This commit is contained in:
parent
77671ade4b
commit
7af516d159
|
@ -0,0 +1,5 @@
|
|||
name=tenant_1
|
||||
datasource.url=jdbc:postgresql://localhost:5432/tenant1
|
||||
datasource.username=postgres
|
||||
datasource.password=123456
|
||||
datasource.driver-class-name=org.postgresql.Driver
|
|
@ -0,0 +1,5 @@
|
|||
name=tenant_2
|
||||
datasource.url=jdbc:postgresql://localhost:5432/tenant2
|
||||
datasource.username=postgres
|
||||
datasource.password=123456
|
||||
datasource.driver-class-name=org.postgresql.Driver
|
|
@ -21,6 +21,11 @@
|
|||
<artifactId>spring-boot-starter</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
|
@ -42,6 +47,16 @@
|
|||
<artifactId>spring-context</artifactId>
|
||||
<version>${org.springframework.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt</artifactId>
|
||||
<version>0.9.1</version>
|
||||
</dependency>
|
||||
<!-- persistence -->
|
||||
<dependency>
|
||||
<groupId>org.hibernate</groupId>
|
||||
|
@ -53,6 +68,12 @@
|
|||
<artifactId>h2</artifactId>
|
||||
<version>${h2.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.4.0</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat</groupId>
|
||||
<artifactId>tomcat-dbcp</artifactId>
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package com.baeldung.multitenant;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
|
||||
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
|
||||
public class MultiTenantApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MultiTenantApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package com.baeldung.multitenant.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.jdbc.DataSourceBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
@Configuration
|
||||
public class MultitenantConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConfigurationProperties(prefix = "tenants")
|
||||
public DataSource dataSource() {
|
||||
File[] files = Paths.get("allTenants").toFile().listFiles();
|
||||
Map<Object, Object> resolvedDataSources = new HashMap<>();
|
||||
|
||||
for (File propertyFile : files) {
|
||||
Properties tenantProperties = new Properties();
|
||||
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
|
||||
|
||||
try {
|
||||
tenantProperties.load(new FileInputStream(propertyFile));
|
||||
String tenantId = tenantProperties.getProperty("name");
|
||||
|
||||
dataSourceBuilder.driverClassName(tenantProperties.getProperty("datasource.driver-class-name"));
|
||||
dataSourceBuilder.username(tenantProperties.getProperty("datasource.username"));
|
||||
dataSourceBuilder.password(tenantProperties.getProperty("datasource.password"));
|
||||
dataSourceBuilder.url(tenantProperties.getProperty("datasource.url"));
|
||||
resolvedDataSources.put(tenantId, dataSourceBuilder.build());
|
||||
} catch (IOException exp) {
|
||||
throw new RuntimeException("Problem in tenant datasource:" + exp);
|
||||
}
|
||||
}
|
||||
|
||||
AbstractRoutingDataSource dataSource = new MultitenantDataSource();
|
||||
dataSource.setDefaultTargetDataSource(resolvedDataSources.get("tenant_1"));
|
||||
dataSource.setTargetDataSources(resolvedDataSources);
|
||||
|
||||
dataSource.afterPropertiesSet();
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package com.baeldung.multitenant.config;
|
||||
|
||||
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
|
||||
|
||||
public class MultitenantDataSource extends AbstractRoutingDataSource {
|
||||
|
||||
@Override
|
||||
protected String determineCurrentLookupKey() {
|
||||
return TenantContext.getCurrentTenant();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package com.baeldung.multitenant.config;
|
||||
|
||||
public class TenantContext {
|
||||
|
||||
private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();
|
||||
|
||||
public static String getCurrentTenant() {
|
||||
return CURRENT_TENANT.get();
|
||||
}
|
||||
|
||||
public static void setCurrentTenant(String tenant) {
|
||||
CURRENT_TENANT.set(tenant);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package com.baeldung.multitenant.config;
|
||||
|
||||
import com.baeldung.multitenant.security.AuthenticationService;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
@Order(1)
|
||||
class TenantFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response,
|
||||
FilterChain chain) throws IOException, ServletException {
|
||||
|
||||
String tenant = AuthenticationService.getTenant((HttpServletRequest) request);
|
||||
TenantContext.setCurrentTenant(tenant);
|
||||
|
||||
try {
|
||||
chain.doFilter(request, response);
|
||||
} finally {
|
||||
TenantContext.setCurrentTenant("");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package com.baeldung.multitenant.controller;
|
||||
|
||||
import com.baeldung.multitenant.domain.Employee;
|
||||
import com.baeldung.multitenant.repository.EmployeeRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@Transactional
|
||||
public class EmployeeController {
|
||||
|
||||
@Autowired
|
||||
private EmployeeRepository employeeRepository;
|
||||
|
||||
@PostMapping(path = "/employee")
|
||||
public ResponseEntity<?> createEmployee() {
|
||||
Employee newEmployee = new Employee();
|
||||
newEmployee.setName("Baeldung");
|
||||
employeeRepository.save(newEmployee);
|
||||
return ResponseEntity.ok(newEmployee);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package com.baeldung.multitenant.domain;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
|
||||
@Entity
|
||||
@Table(name = "employee")
|
||||
public class Employee {
|
||||
|
||||
@Id
|
||||
@Column(name = "id")
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "name")
|
||||
private String name;
|
||||
|
||||
public Employee() {
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.baeldung.multitenant.repository;
|
||||
|
||||
import com.baeldung.multitenant.domain.Employee;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.baeldung.multitenant.security;
|
||||
|
||||
public class AccountCredentials {
|
||||
|
||||
private String username;
|
||||
private String password;
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package com.baeldung.multitenant.security;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.filter.GenericFilterBean;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
|
||||
public class AuthenticationFilter extends GenericFilterBean {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
|
||||
Authentication authentication = AuthenticationService.getAuthentication((HttpServletRequest) req);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
chain.doFilter(req, res);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package com.baeldung.multitenant.security;
|
||||
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
|
||||
public class AuthenticationService {
|
||||
|
||||
private static final long EXPIRATIONTIME = 864_000_00; // 1 day in milliseconds
|
||||
private static final String SIGNINGKEY = "SecretKey";
|
||||
private static final String PREFIX = "Bearer";
|
||||
|
||||
public static void addToken(HttpServletResponse res, String username, String tenant) {
|
||||
String JwtToken = Jwts.builder().setSubject(username)
|
||||
.setAudience(tenant)
|
||||
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
|
||||
.signWith(SignatureAlgorithm.HS512, SIGNINGKEY)
|
||||
.compact();
|
||||
res.addHeader("Authorization", PREFIX + " " + JwtToken);
|
||||
}
|
||||
|
||||
public static Authentication getAuthentication(HttpServletRequest req) {
|
||||
String token = req.getHeader("Authorization");
|
||||
if (token != null) {
|
||||
String user = Jwts.parser()
|
||||
.setSigningKey(SIGNINGKEY)
|
||||
.parseClaimsJws(token.replace(PREFIX, ""))
|
||||
.getBody()
|
||||
.getSubject();
|
||||
if (user != null) {
|
||||
return new UsernamePasswordAuthenticationToken(user, null, Collections.emptyList());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String getTenant(HttpServletRequest req) {
|
||||
String token = req.getHeader("Authorization");
|
||||
if (token == null) {
|
||||
return null;
|
||||
}
|
||||
String tenant = Jwts.parser()
|
||||
.setSigningKey(SIGNINGKEY)
|
||||
.parseClaimsJws(token.replace(PREFIX, ""))
|
||||
.getBody()
|
||||
.getAudience();
|
||||
return tenant;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package com.baeldung.multitenant.security;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
public class LoginFilter extends AbstractAuthenticationProcessingFilter {
|
||||
|
||||
public LoginFilter(String url, AuthenticationManager authManager) {
|
||||
super(new AntPathRequestMatcher(url));
|
||||
setAuthenticationManager(authManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
|
||||
throws AuthenticationException, IOException, ServletException {
|
||||
|
||||
AccountCredentials creds = new ObjectMapper().
|
||||
readValue(req.getInputStream(), AccountCredentials.class);
|
||||
|
||||
return getAuthenticationManager().authenticate(
|
||||
new UsernamePasswordAuthenticationToken(creds.getUsername(),
|
||||
creds.getPassword(), Collections.emptyList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res,
|
||||
FilterChain chain, Authentication auth) throws IOException, ServletException {
|
||||
|
||||
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
|
||||
String tenant = "";
|
||||
for (GrantedAuthority gauth : authorities) {
|
||||
tenant = gauth.getAuthority();
|
||||
}
|
||||
|
||||
AuthenticationService.addToken(res, auth.getName(), tenant.substring(5));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package com.baeldung.multitenant.security;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Override
|
||||
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
|
||||
auth.inMemoryAuthentication()
|
||||
.passwordEncoder(passwordEncoder())
|
||||
.withUser("user")
|
||||
.password(passwordEncoder().encode("baeldung"))
|
||||
.roles("tenant_1");
|
||||
|
||||
auth.inMemoryAuthentication()
|
||||
.passwordEncoder(passwordEncoder())
|
||||
.withUser("admin")
|
||||
.password(passwordEncoder().encode("baeldung"))
|
||||
.roles("tenant_2");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder(){
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeRequests()
|
||||
.antMatchers("/login").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.sessionManagement()
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
.and()
|
||||
.addFilterBefore(new LoginFilter("/login", authenticationManager()),
|
||||
UsernamePasswordAuthenticationFilter.class)
|
||||
.addFilterBefore(new AuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
|
||||
.csrf().disable()
|
||||
.headers().frameOptions().disable()
|
||||
.and()
|
||||
.httpBasic();
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
create table employee (id int8 generated by default as identity, name varchar(255), primary key (id));
|
Loading…
Reference in New Issue