Combine roteamento dinâmico de fonte de dados com spring-data-rest
-
02-01-2020 - |
Pergunta
Estou usando o roteamento dinâmico de fonte de dados conforme indicado nesta postagem do blog:http://spring.io/blog/2007/01/23/dynamic-datasource-routing/
Isso funciona bem, mas quando eu combino com spring-data-rest
e navegando em meus repositórios gerados, recebo (com razão) uma exceção de que minha chave de pesquisa não está definida (não defino um padrão).
Como e onde posso conectar-me ao tratamento de solicitação de descanso de dados do Spring para definir a chave de pesquisa com base em 'x' (autorizações de usuário, prefixo de caminho ou outro), antes de qualquer conexão ser feita com o banco de dados?
Em termos de código, minha configuração de fonte de dados corresponde principalmente à postagem do blog na parte superior, com algumas classes de entidade básicas, repositórios gerados e Spring Boot para agrupar tudo.Se necessário eu poderia postar algum código, mas não há muito para ver lá.
Solução
Minha primeira ideia é aproveitar o Spring Security authentication
objeto para definir a fonte de dados atual com base em authorities
anexado à autenticação.Claro, você pode colocar a chave de pesquisa em um formato personalizado UserDetails
objeto ou até mesmo um objeto de autenticação personalizado também.Por uma questão de brevidade, concentrar-me-ei numa solução baseada nas autoridades.Esta solução requer um objeto de autenticação válido (o usuário anônimo também pode ter uma autenticação válida).Dependendo da configuração do Spring Security, a alteração da autoridade/fonte de dados pode ser realizada por solicitação ou sessão.
Minha segunda idéia é trabalhar com um javax.servlet.Filter
para definir a chave de pesquisa em uma variável local de thread antes que o Spring Data Rest entre em ação.Esta solução é independente da estrutura e pode ser usada por solicitação ou sessão.
Roteamento de fonte de dados com Spring Security
Usar SecurityContextHolder
para acessar as autoridades de autenticação atuais.Com base nas autoridades, decida qual fonte de dados usar.Assim como seu código, não estou definindo um defaultTargetDataSource no meu 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);
}
}
No seu código você deve substituir o na memória UserDetailsService
(inMemoryAuthentication) com um UserDetailsService que atende às suas necessidades.Mostra que existem dois usuários diferentes com funções diferentes TENANT1
e TENANT2
usado para o roteamento da fonte de dados.
@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();
}
}
Aqui está um exemplo completo: https://github.com/ksokol/spring-sandbox/tree/sdr-routing-datasource-spring-security/spring-data
Roteamento de fonte de dados com javax.servlet.Filter
Crie uma nova classe de filtro e adicione-a ao seu web.xml
ou registre-o no 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;
}
}
A classe de locatário é um wrapper simples em torno de um objeto estático 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(); }
}
Assim como seu código, não estou definindo um defaultTargetDataSource em meu AbstractRoutingDataSource.
public class CustomRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
if(Tenant.getCurrentTenant() == null) {
return "TENANT1";
}
return Tenant.getCurrentTenant().toUpperCase();
}
}
Agora você pode alternar a fonte de dados com http://localhost:8080/sandbox/myEntities;tenant=tenant1
.Cuidado, pois o locatário deve ser definido em todas as solicitações.Alternativamente, você pode armazenar o inquilino no HttpSession
para solicitações subsequentes.
Aqui está um exemplo completo: https://github.com/ksokol/spring-sandbox/tree/sdr-routing-datasource-url/spring-data