Search code examples
javaspring-dataspring-data-jpaspring-data-rest

Combine Dynamic datasource routing with spring-data-rest


I'm using Dynamic datasource routing as indicated in this blog post: http://spring.io/blog/2007/01/23/dynamic-datasource-routing/

This works fine, but when I combine it with spring-data-rest and browsing of my generated repositories I (rightfully) get an exception that my lookup-key is not defined (I do not set a default).

How and where can I hook into the Spring data rest request handling to set the lookup-key based on 'x' (user authorizations, path prefix, or other), before any connection is made to the database?

Code-wise my datasource configuration just mostly matches the blogpost at the top, with some basic entity classes, generated repositories and Spring Boot to wrap everything together. If need I could post some code, but there's nothing much to see there.


Solution

  • My first idea is to leverage Spring Security's authentication object to set current datasource based on authorities attached to the authentication. Of course, you can put the lookup key in a custom UserDetails object or even a custom Authentication object, too. For sake of brevity I`ll concentrate on a solution based on authorities. This solution requires a valid authentication object (anonymous user can have a valid authentication, too). Depending on your Spring Security configuration changing authority/datasource can be accomplished on a per request or session basis.

    My second idea is to work with a javax.servlet.Filter to set lookup key in a thread local variable before Spring Data Rest kicks in. This solution is framework independent and can be used on a per request or session basis.

    Datasource routing with Spring Security

    Use SecurityContextHolder to access current authentication's authorities. Based on the authorities decide which datasource to use. Just as your code I'm not setting a defaultTargetDataSource on my AbstractRoutingDataSource.

    public class CustomRoutingDataSource extends AbstractRoutingDataSource {
    
        @Override
        protected Object determineCurrentLookupKey() {
            Set<String> authorities = getAuthoritiesOfCurrentUser();
            if(authorities.contains("ROLE_TENANT1")) {
                return "TENANT1";
            }
            return "TENANT2";
        }
    
        private Set<String> getAuthoritiesOfCurrentUser() {
            if(SecurityContextHolder.getContext().getAuthentication() == null) {
                return Collections.emptySet();
            }
            Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
            return AuthorityUtils.authorityListToSet(authorities);
        }
    }
    

    In your code you must replace the in memory UserDetailsService (inMemoryAuthentication) with a UserDetailsService that serves your need. It shows you that there are two different users with different roles TENANT1 and TENANT2 used for the datasource routing.

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
            auth
                .inMemoryAuthentication()
                .withUser("user1").password("user1").roles("USER", "TENANT1")
                .and()
                .withUser("user2").password("user2").roles("USER", "TENANT2");
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.antMatcher("/**")
                .authorizeRequests()
                .antMatchers("/**").hasRole("USER")
                .and()
                .httpBasic()
                .and().csrf().disable();
        }
    }
    

    Here is a complete example: https://github.com/ksokol/spring-sandbox/tree/sdr-routing-datasource-spring-security/spring-data

    Datasource routing with javax.servlet.Filter

    Create a new filter class and add it to your web.xml or register it with the AbstractAnnotationConfigDispatcherServletInitializer, respectively.

    public class TenantFilter implements Filter {
    
        private final Pattern pattern = Pattern.compile(";\\s*tenant\\s*=\\s*(\\w+)");
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            String tenant = matchTenantSystemIDToken(httpRequest.getRequestURI());
            Tenant.setCurrentTenant(tenant);
            try {
                chain.doFilter(request, response);
            } finally {
                Tenant.clearCurrentTenant();
            }
        }
    
        private String matchTenantSystemIDToken(final String uri) {
            final Matcher matcher = pattern.matcher(uri);
            if (matcher.find()) {
                return matcher.group(1);
            }
            return null;
        }
    }
    

    Tenant class is a simple wrapper around a static ThreadLocal.

    public class Tenant {
    
        private static final ThreadLocal<String> TENANT = new ThreadLocal<>();
    
        public static void setCurrentTenant(String tenant) { TENANT.set(tenant); }
    
        public static String getCurrentTenant() { return TENANT.get(); }
    
        public static void clearCurrentTenant() { TENANT.remove(); }
    }
    

    Just as your code I`m not setting a defaultTargetDataSource on my AbstractRoutingDataSource.

    public class CustomRoutingDataSource extends AbstractRoutingDataSource {
    
        @Override
        protected Object determineCurrentLookupKey() {
            if(Tenant.getCurrentTenant() == null) {
                return "TENANT1";
            }
            return Tenant.getCurrentTenant().toUpperCase();
        }
    }
    

    Now you can switch datasource with http://localhost:8080/sandbox/myEntities;tenant=tenant1. Beware that tenant has to be set on every request. Alternatively, you can store the tenant in the HttpSession for subsequent requests.

    Here is a complete example: https://github.com/ksokol/spring-sandbox/tree/sdr-routing-datasource-url/spring-data