Kombinieren Sie dynamisches Datenquellen-Routing mit Spring-Data-Rest
-
02-01-2020 - |
Frage
Ich verwende dynamisches Datenquellen-Routing, wie in diesem Blogbeitrag angegeben:http://spring.io/blog/2007/01/23/dynamic-datasource-routing/
Das funktioniert gut, aber wenn ich es kombiniere mit spring-data-rest
beim Durchsuchen meiner generierten Repositorys erhalte ich (zu Recht) eine Ausnahme, dass mein Suchschlüssel nicht definiert ist (ich setze keinen Standard).
Wie und wo kann ich mich in die Spring Data Rest-Anforderungsverarbeitung einbinden, um den Suchschlüssel basierend auf 'x' (Benutzerberechtigungen, Pfadpräfix oder andere) festzulegen, bevor eine Verbindung zur Datenbank hergestellt wird?
In Bezug auf den Code stimmt meine Datenquellenkonfiguration größtenteils mit dem Blogpost oben überein, mit einigen grundlegenden Entitätsklassen, generierten Repositorys und Spring Boot, um alles zusammenzufassen.Bei Bedarf könnte ich einen Code posten, aber dort gibt es nicht viel zu sehen.
Lösung
Meine erste Idee ist es, Spring Security zu nutzen authentication
objekt zum Festlegen der aktuellen Datenquelle basierend auf authorities
an die Authentifizierung angehängt.Natürlich können Sie den Suchschlüssel in eine benutzerdefinierte UserDetails
objekt oder sogar ein benutzerdefiniertes Authentifizierungsobjekt.Der Kürze halber werde ich mich auf eine Lösung konzentrieren, die auf Behörden basiert.Diese Lösung erfordert ein gültiges Authentifizierungsobjekt (anonymer Benutzer kann auch eine gültige Authentifizierung haben).Abhängig von Ihrer Spring-Sicherheitskonfiguration kann das Ändern der Berechtigung / Datenquelle auf Anfrage- oder Sitzungsbasis durchgeführt werden.
Meine zweite Idee ist, mit einem zu arbeiten javax.servlet.Filter
so legen Sie den Suchschlüssel in einer lokalen Thread-Variablen fest, bevor Spring Data Rest aktiviert wird.Diese Lösung ist Framework-unabhängig und kann auf Anfrage- oder Sitzungsbasis verwendet werden.
Datenquellen-Routing mit Spring Security
Verwenden SecurityContextHolder
zugriff auf die Berechtigungen der aktuellen Authentifizierung.Basierend darauf entscheiden die Behörden, welche Datenquelle verwendet werden soll.Genau wie Ihr Code setze ich keine defaultTargetDataSource auf meinem 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 Ihrem Code müssen Sie den Speicher ersetzen UserDetailsService
(inMemoryAuthentication) mit einem UserDetailsService, der Ihren Anforderungen entspricht.Es zeigt Ihnen, dass es zwei verschiedene Benutzer mit unterschiedlichen Rollen gibt TENANT1
und TENANT2
wird für das Datenquellen-Routing verwendet.
@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();
}
}
Hier ist ein vollständiges Beispiel: https://github.com/ksokol/spring-sandbox/tree/sdr-routing-datasource-spring-security/spring-data
Datenquellen-Routing mit Javax.ein Servlet.Filter
Erstellen Sie eine neue Filterklasse und fügen Sie sie Ihrer hinzu web.xml
oder registrieren Sie es mit der AbstractAnnotationConfigDispatcherServletInitializer
, jeweils.
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;
}
}
Mandantenklasse ist ein einfacher Wrapper um eine statische 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(); }
}
Genau wie Ihr Code setze ich keine defaultTargetDataSource für meine AbstractRoutingDataSource .
public class CustomRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
if(Tenant.getCurrentTenant() == null) {
return "TENANT1";
}
return Tenant.getCurrentTenant().toUpperCase();
}
}
Jetzt können Sie die Datenquelle wechseln mit http://localhost:8080/sandbox/myEntities;tenant=tenant1
.Beachten Sie, dass der Mieter bei jeder Anfrage festgelegt werden muss.Alternativ können Sie den Mandanten im HttpSession
für spätere Anfragen.
Hier ist ein vollständiges Beispiel: https://github.com/ksokol/spring-sandbox/tree/sdr-routing-datasource-url/spring-data