Neuigkeiten von trion.
Immer gut informiert.

Method Security in Spring: User für asynchrone und zeitgesteuerte Aufgaben

Mit Spring Security können unter anderem Webanwendungen (Spring WebMVC) auf einfache und flexible Weise abgesichert werden. Dabei ist der typische Weg über Security-Filter-Chains die Webschicht von Spring WebMVC mit Spring Security abzusichern. Doch reicht das aus?
Diese rhetorische Frage lässt sich mit vielleicht beantworten: Gibt es keine Programmierfehler oder Logikfehler, so kann das bereits ausreichen. Denn erreicht kein Request unberechtigt die Controller-Schicht, dann kann nichts passieren.

Doch in Zeiten zunehmender Cyberbedrohungen sollte keine Anwendung lediglich durch eine Schicht gesichert werden. Verteidigung in der Tiefe kann dank Spring Security leichtgewichtig mit Method Security umsetzen. Eine deutliche Verbesserung.

In unserem letzten Spring Security Training kam die Frage auf, wie sich dies mit periodischen Aufgaben innerhalb der Spring Anwendung kombinieren lässt. Das soll als Anlass dienen, sowohl periodische als auch asynchrone Ausführung, z.B. durch Message-Queue Nachrichten in Kombination mit Method Security darzustellen.

Method Security

Durch Spring Security kann neben dem Aufruf einer Methode auch das Resultat flexibel validiert werden. Damit lassen sich effektive Schutzmechanismen implementieren.
Der folgende Programmcode zeigt, wie eine Methode einer Spring Komponente, hier ist es ein @Service, durch Spring Security abgesichert werden kann.

Absicherung eines Methodenaufrufs mit Spring Security
@PreAuthorize("hasRole('ADMIN')")
public ApiData write(String input)
{
    final var invoker = SecurityContextHolder.getContext().getAuthentication().getName();
    logger.info("Service writing {}/{}", Thread.currentThread().getName(), invoker);

    return new ApiData("write-result with: " + input);
}

Die write Methode kann nunmehr lediglich aufgerufen werden, wenn der Aufrufer authentifiziert ist und die Rolle ADMIN hat.

Der Spring Security Context

Zunächst ist es fundamental zu verstehen, wie Spring Security arbeitet: Im klassischen Fall nicht-reaktiver Anwendungen wird der sogenannte Security-Context verwendet, um dort die für den aktuellen Ablauf (z.B. HTTP Request) gültige Authentication inkl. Userdetails und Authorities bereitzustellen. Der SecurityContext wird dabei methodenübergreifend durch den SecurityContextHolder bereitgestellt. Damit spart man sich, sämtliche Methodensignaturen mit dem zusätzlichen Parameter zu versehen, der zumal nicht zwingend auch genutzt wird.

Zur Umsetzung delegiert der SecurityContextHolder an eine von mehreren Strategien. Standardmäßig wird dazu ein ThreadLocal verwendet, der SecurityContext ist also pro Thread von anderen Instanzen separiert. Das passt sehr gut zu Webanwendungen, die typischerweise einen Thread pro HTTP-Request exklusiv nutzen.
Bei reaktiven Anwendungen, die viele HTTP Requests mit wenigen Threads abbilden stellt Spring Security ein dazu passendes ähnliches Konzept bereit.

Falls kein SecurityContext vorhanden ist, verhält sich Spring Security so, als wenn ein unauthentifizierter Aufruf stattfindet. Entsprechend stehen auch keine Authorities zur Verfügung und eine Autorisierung wird in der Regel verweigert werden.
Hier ist wichtig, sich klarzumachen, wie diese beiden Aspekte zusammenhängen: Wird zur Ausführung der Thread gewechselt, werden die ThreadLocal Werte nicht automatisch propagiert. Damit steht für die weitere Ausführung der SecurityContext und damit die Authentifizierung nicht mehr zur Verfügung.

Im Folgenden betrachten wir einmal verschiedene Szenarien, bei denen es zu einer solchen Ausführung kommt, und wie ein Entwickler dies bei Spring Security beeinflussen kann.
Übrigens: Ähnliche Herausforderungen stellen sich im richtigen Umgang mit Transaktionen und @Transactional. Selten ist die Investition in Know-How so relevant, wie bei diesen beiden Themen.

Asynchrone Servlets

Seit der Java Servlet Spezifikation 3 ist es möglich, asynchron auf Requests zu reagieren. Die Idee dabei ist, dass der Servletcontainer bei vielen gleichzeitigen Anfragen nicht für jeden Request einen separaten Thread bereithalten muss. Durch die asynchrone Servlet API wird ein AsyncContext bereitgestellt, der an andere Threads propagiert werden kann und auch Listener zur Integration bereitstellt. Nachfolgend ist die API skizziert.

Beispiel API asynchrones Servlet
final AsyncContext asyncContext = request.startAsync(request, response);
...
var out = asyncContext.getResponse().getWriter();
...
asyncContext.complete();

In welchen Situationen und bei welchem Anwendungsdesign dieser Ansatz echte Vorteile bietet soll an dieser Stelle nicht weiter ausgeführt werden.

Spring WebMVC bietet für diese asynchronen Servlets eine sehr praktische Unterstützung, ohne dass das Programmiermodell sich dabei deutlich anders darstellt: Als @Controller Rückgabewert muss lediglich ein Callable<T> oder ein DeferredResult<T> als Typ verwendet werden.

Spring WebMVC mit asynchronem Servlet
@GetMapping
public Callable<ApiData> get()
{
    logger.info("Deferred read request {}", Thread.currentThread().getName()); // z.B. http-nio-8080-exec-1
    Callable<ApiData> result = apiService::read;
    logger.info("Deferred read result {}", Thread.currentThread().getName()); // z.B. http-nio-8080-exec-1
    return result;
}

Spring Security integriert sich hier ebenfalls, jedoch ausschließlich, falls ein Callable<> zurückgeliefert wird, nicht bei DeferredResult<T>. Der Hintergrund dabei ist, dass das Callable durch Spring aufgerufen wird und dabei Spring auch die Kontrolle über den zu verwendenden Thread(-Pool) und die Verwaltung von Thread-Locals hat.

Hier gibt es auch direkt einen möglichen Stolperstein:
Spring WebMVC erkennt zu den eben erwähnten Typen auch automatisch CompletableFuture und ListenableFuture und löst diese auf. Dazu werden diese über asynchrone Servlets abgebildet oder es wird mit einem Request-Thread blockierend auf das Ergebnis gewartet.

Controller mit CompletableFuture Rückgabetyp
@GetMapping("/async")
public CompletableFuture<ApiData> asyncGet() throws InterruptedException
{
    CompletableFuture<ApiData> result = asyncApiService.read();
    return result;
}

Auch hier koordiniert Spring WebMVC die für Spring Security wichtigen ThreadLocal Daten, so dass es nicht zu Problemen kommt. Aber bei der Bereitstellung der Daten im CompletableFuture kann es sehr wohl zu Situationen kommen, die zu unerwartetem Verhalten oder sogar Sicherheitslücken führen können. Ein Beispiel ist die Verwendung von supplyAsync(), dabei wird der Standard Fork-Join Threadpool verwendet - ThreadLocals werden dabei nicht berücksichtigt. Und auch im Kontext weiterer Spring Mechanismen lauert Fehlerpotential.

Asynchrone Ausführung mit @Async

Zur asynchronen Ausführung von Methoden bietet Spring die @Async-Annotation. Zusammen mit @EnableAsync an einer @Configuration wird die Ausführung auf separate Threads ausgelagert. Die Threads stellt Spring - wie sollte es anders sein - durch eine separate Abstraktion bereit. Dabei kommt standardmäßig ein gecachter Threadpool zum Einsatz, mit Java 21 können auch virtuelle Threads verwendet werden. Und genau hier kann es nun zu der Situation kommen, dass bei der asynchronen Ausführung keine Weitergabe des SecurityContext erfolgt.

Wird dann noch zusätzlich mit Method Security gearbeitet, so fehlen die Informationen und die Autorisierung wird abgelehnt. Schlimmer, falls ein Thread aus dem Pool recycled wird und dieser aus einer vorherigen Nutzung entsprechende ThreadLocals besitzt, können fehlerhafte Autorisierungen erfolgen.

Service mit @Async Methode und Method Security
@Async
@PreAuthorize("hasRole('ADMIN')")
public CompletableFuture<ApiData> write(String input) throws InterruptedException
{
    logger.info("Async service starting {}", Thread.currentThread().getName());

    TimeUnit.SECONDS.sleep(2);
    return CompletableFuture.completedFuture(new ApiData("write-result with: " + input));
}

Abhilfe schafft hier einen entsprechenden Executor bereitzustellen, der darauf achtet, dass der SecurityContext korrekt propagiert - und auch wieder abgeräumt - wird. Dazu bietet Spring den DelegatingSecurityContextAsyncTaskExecutor, der ganz einfach als Bean bereitgestellt werden kann.

Bereitstellung eines Executor mit Berücksichtigung des SecurityContext als Bean
@Bean
public DelegatingSecurityContextAsyncTaskExecutor asyncTaskExecutor(ThreadPoolTaskExecutor delegate)
{
    return new DelegatingSecurityContextAsyncTaskExecutor(delegate);
}

Damit dieser Executor auch genutzt wird, wird der Qualifier der Bean in der @Async-Annotation als Parameter mitgegeben. Da der Bean-Name der Standardqualifier ist, kann dieser am einfachsten verwendet werden.
Ein Beispiel ist hier zu sehen:

Referenzierung über den Bean-Namen
@Async("asyncTaskExecutor")
@PreAuthorize("hasRole('ADMIN')")
public CompletableFuture<ApiData> write(String input) throws InterruptedException
{
    final var invoker = SecurityContextHolder.getContext().getAuthentication().getName();
    logger.info("Async service starting {}/{}", Thread.currentThread().getName(), invoker);

    TimeUnit.SECONDS.sleep(2);
    return CompletableFuture.completedFuture(new ApiData("write-result with: " + input));
}

Im Prinzip dieselbe Lösung kann analog für weitere Arten der Ausführung gewählt werden, bei denen Spring Security Informationen bereitgestellt oder weitergegeben werden müssen. Schauen wir zuletzt auf die Fragestellung, die als Motivation zu diesem Beitrag diente: Wie ist es bei zeitgesteuerter Ausführung und Spring Security Method Security.

Zeitgesteuerte Ausführung

Spring stellt zur zeitgesteuerten Ausführung ebenfalls Abstraktionen bereit. Ein Weg zur Konfiguration ist die @Scheduled Annotation.
Dabei kann zwischen verschiedenen Arten der Konfiguration gewählt werden, z.B. periodische Ausführung auf Basis fester Zeitintervalle oder auch die Verwendung von Cron-Ausdrücken. Das folgende Beispiel zeigt einen solchen Task, der alle zwei Minuten ausgeführt wird.

Service mit zeitgesteuerter Methode
@Scheduled(
        cron = "0 */2 * * * *"   // every two minutes -- second minute hour day-of-month month day-of-week
)
public void scheduled()
{
    logger.info("Scheduled service invocation {}", Thread.currentThread().getName());
    var result = apiService.write("scheduled");
    logger.info("Scheduled result: {}", result);
}

Falls der dann aufgerufene Service durch Method Security abgesichert ist, kommt es zu einer Exception: Es fehlen die zur Autorisierung erforderlichen Informationen da kein SecurityContext vorhanden ist. Analog zu dem für die asynchrone Ausführung eingeführten Ansatz kann wieder ein Executor-Service als Bean bereitgestellt werden. Da hier das System selbst der Akteur ist, ist eine feste Vorbelegung des technischen Users und seiner Authorities sinnvoll. Der folgende Quellcode zeigt, wie eine solche Konfiguration als Spring Bean aussehen könnte.

Bereitstellung eines technischen Users für zeitgesteuerte Ausführungen
@Bean
public DelegatingSecurityContextScheduledExecutorService adminScheduledTaskExecutor()
{
    final var delegate = Executors.newSingleThreadScheduledExecutor();

    final Collection<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_ADMIN");
    final Authentication authentication = new AnonymousAuthenticationToken(
            "<spring-user-id>",
            "<scheduler-user>",
            authorities
    );

    final SecurityContext context = SecurityContextHolder.createEmptyContext();
    context.setAuthentication(authentication);
    return new DelegatingSecurityContextScheduledExecutorService(delegate, context);
}

Diese Scheduler-Bean kann nun analog zur @Async Annotation in der @Scheduled Annotation referenziert werden. Auch hier wird der Bean Qualifier erwartet, im einfachsten Fall der Bean-Name. Im folgenden Quellcodeausschnitt ist die Konfiguration auf der Seite des zeitgesteuerten Service zu sehen.

Service mit zeitgesteuerter Methode
@Scheduled(
        cron = "0 */2 * * * *",   // every two minutes -- second minute hour day-of-month month day-of-week
        scheduler = "adminScheduledTaskExecutor"
)
public void scheduled()
{
    logger.info("Scheduled service invocation {}", Thread.currentThread().getName());
    var result = apiService.write("scheduled");
    logger.info("Scheduled result: {}", result);
}

Nach diesen Beispielen wird es sicherlich leichter einzusehen, dass es sehr wichtig ist, sich mit den zugrundeliegenden Funktionsweisen von Spring Security vertraut zu machen. Vor allem bei der Interaktion zwischen verschiedenen Konzepten, wie Spring Security und Messaging oder asynchroner Ausführung gilt es die Wechselwirkungen zu verstehen, um Spring Security zu beherrschen. Wie immer gilt auch an dieser Stelle: Wer ganz auf Nummer sicher gehen will, der erstellt dazu passende automatisch ausgeführte Tests. Sowohl für den "Gut-Fall", als auch erwartete Fehlschläge. Dann ist das Projekt auch bei umfangreichen Erweiterungen, Aktualisierungen und Refactorings gegen Regressionen gewappnet.




Zu den Themen Spring Security, Spring Boot, OWASP Web Security und Keycloak bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:

Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.

Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Zur Desktop Version des Artikels