Combine el enrutamiento dinámico de fuentes de datos con spring-data-rest
-
02-01-2020 - |
Pregunta
Estoy usando enrutamiento dinámico de fuente de datos como se indica en esta publicación de blog:http://spring.io/blog/2007/01/23/dynamic-datasource-routing/
Esto funciona bien, pero cuando lo combino con spring-data-rest
y al explorar mis repositorios generados, obtengo (con razón) una excepción de que mi clave de búsqueda no está definida (no establezco un valor predeterminado).
¿Cómo y dónde puedo conectarme al manejo de solicitudes de resto de datos de Spring para configurar la clave de búsqueda basada en 'x' (autorizaciones de usuario, prefijo de ruta u otro), antes de realizar cualquier conexión a la base de datos?
En cuanto al código, mi configuración de fuente de datos coincide principalmente con la publicación del blog en la parte superior, con algunas clases de entidad básicas, repositorios generados y Spring Boot para envolver todo.Si es necesario, podría publicar algún código, pero no hay mucho que ver allí.
Solución
Mi primera idea es aprovechar la tecnología de Spring Security. authentication
objeto para establecer la fuente de datos actual en función de authorities
adjunto a la autenticación.Por supuesto, puedes poner la clave de búsqueda en un archivo personalizado. UserDetails
objeto o incluso un objeto de autenticación personalizado también.En aras de la brevedad, me concentraré en una solución basada en autoridades.Esta solución requiere un objeto de autenticación válido (el usuario anónimo también puede tener una autenticación válida).Dependiendo de su configuración de Spring Security, el cambio de autoridad/fuente de datos se puede realizar por solicitud o sesión.
Mi segunda idea es trabajar con un javax.servlet.Filter
para establecer la clave de búsqueda en una variable local de hilo antes de que se active Spring Data Rest.Esta solución es independiente del marco y se puede utilizar por solicitud o sesión.
Enrutamiento de fuentes de datos con Spring Security
Usar SecurityContextHolder
para acceder a las autoridades de autenticación actuales.Las autoridades deciden qué fuente de datos utilizar.Al igual que su código, no estoy configurando un defaultTargetDataSource en mi 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);
}
}
En tu código debes reemplazar el en memoria. UserDetailsService
(inMemoryAuthentication) con un UserDetailsService que satisfaga sus necesidades.Te muestra que hay dos usuarios diferentes con roles diferentes. TENANT1
y TENANT2
utilizado para el enrutamiento de la fuente de datos.
@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();
}
}
Aquí tienes un ejemplo completo: https://github.com/ksokol/spring-sandbox/tree/sdr-routing-datasource-spring-security/spring-data
Enrutamiento de fuente de datos con javax.servlet.Filter
Cree una nueva clase de filtro y agréguela a su web.xml
o registrarlo en el AbstractAnnotationConfigDispatcherServletInitializer
, respectivamente.
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;
}
}
La clase Tenant es un contenedor simple alrededor de una clase estática. 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(); }
}
Al igual que su código, no estoy configurando un defaultTargetDataSource en mi AbstractRoutingDataSource.
public class CustomRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
if(Tenant.getCurrentTenant() == null) {
return "TENANT1";
}
return Tenant.getCurrentTenant().toUpperCase();
}
}
Ahora puedes cambiar la fuente de datos con http://localhost:8080/sandbox/myEntities;tenant=tenant1
.Tenga en cuenta que el inquilino debe configurarse en cada solicitud.Alternativamente, puede guardar al inquilino en el HttpSession
para solicitudes posteriores.
Aquí tienes un ejemplo completo: https://github.com/ksokol/spring-sandbox/tree/sdr-routing-datasource-url/spring-data