Keycloak und Spring Security Teil 2: Integration von Keycloak in Spring Security

Keine Kommentare

In diesem Teil des Hands-On werden wir zuerst das Backend unserer Gästebuch-Applikation als Spring-Boot-Service erstellen und dieses dann durch eine Integration von Keycloak in Spring Security absichern. Für diejenigen unter euch, die direkt hier gelandet sind, lohnt es sich bestimmt, vorher auch Teil 1 gelesen zu haben, in dem wir ein Web-Frontend mit Keycloak erstellt und abgesichert haben.

Disclaimer: Ich gehe davon aus, dass der grundsätzliche Umgang mit Spring Boot und Spring Security bekannt ist. Außerdem werde ich hier aus Darstellungsgründen nicht den kompletten Code posten. Sollte etwas für euch Wichtiges fehlen, könnt ihr das gern in Form von Kommentaren mitteilen, dann versuche ich das nachzureichen. Ansonsten findet ihr den Code auch bei GitHub. 🙂

Der Backend-Service

Bisher ist unser Gästebuch eher wenig sinnvoll, da keine Funktionalität hinterlegt ist. Also brauchen wir – natürlich – Services, die uns diese Funktionalität zur Verfügung stellen. Um das Beispiel simpel zu halten, nennen wir unseren Service sehr grobgranular „Backend-App“. Der Service lauscht auf Port 8090 und sorgt dafür, dass Einträge angezeigt und hinzugefügt werden können und dass beim Hinzufügen asynchron eine E-Mail über einen dritten Service versandt wird. Dabei werden wir für die Grundeinrichtung den großartigen Spring Initializr verwenden.

Für das Projekt nutzen wir dazu folgende Starter:

  • Web: Für Web-Funktionalität und Annotationen wie z.B. @RestController
  • Data-JPA & H2: Zur Bereitstellung & Anbindung einer H2-In-Memory-Testdatenbank
  • Security: Integration von Funktionalität zum Schutz unserer Endpunkte vor unbefugtem Zugriff

Dazu benötigen wir zur Keycloak-Integration noch folgende Dependencies, die uns alle benötigten Klassen und Methoden zur Verfügung stellen:

...
<dependency>
	<groupId>org.keycloak</groupId>
	<artifactId>keycloak-spring-security-adapter</artifactId>
	<version>3.2.1.Final</version>
</dependency>
<dependency>
	<groupId>org.keycloak</groupId>
	<artifactId>keycloak-spring-boot-starter</artifactId>
    <version>3.2.1.Final</version>
</dependency>
...

Da ich generell lange pom.xml-Beschreibungen in Blogposts nur selten lesenswert finde, verweise ich hier gern auf die vollständige Datei im Github-Repo.

Projektstruktur

Kurz ein, zwei Worte zu der folgend in Bild 1 gezeigten Projektstruktur der Demo, die dem einen oder anderen etwas komisch vorkommen mag: Ich habe hier versucht, allen Nicht-Domaincode wie zum Beispiel konkrete Persistenz-Implementierungen vom eigentlich benötigten Code für die Funktionalität meines Services (meiner „Domain“) unabhängig zu machen. Ich mag das Ganze noch nicht hexagonale Architektur nennen – dafür ist es zu inkonsistent – aber es ist ein erster Versuch, über den sich gern im Kommentarabschnitt mit mir diskutieren lässt.

project-structure-backend

Bild 1: Projektstruktur der Backend-App – Kritik und Anregungen gern in Form von Kommentaren.

Domain

Ein Gästebucheintrag besteht bei uns aus einer eindeutigen ID, einem Titel, dem Eintrag selbst, dem Namen des Users und einem Datum, das standardmäßig auf das aktuelle Datum gesetzt wird.

public class GuestbookEntry {
 
    private Long id;
 
    private String title;
 
    private String comment;
 
    private String commenter;
 
    @JsonFormat(pattern = "dd.MM.yyyy, HH:mm:ss")
    private Date date = new Date();
 
    //getter & setter

Damit das Datum für uns in Deutschland ohne Weiteres lesbar ist, fügen wir noch mit @JsonFormat das gewünschte Pattern für die Anzeige an – fertig.

Daneben erstellen wir noch einen GuestbookMailService, der die Kommunikation mit dem Mail-Microservice koordiniert:

@Service
public class GuestbookMailService {
 
    private final GuestbookMailClient mailClient;
 
    public GuestbookMailService(GuestbookMailClient mailClient) {
	this.mailClient = mailClient;
    }
 
    @Async
    public void sendMail(final GuestbookEntry entry) {
 
	System.out.println("Sending Mail...");
	mailClient.sendMail(entry);
	System.out.println("Successfully sent Mail!");
 
    }
}

Hierbei möchten wir nicht, dass unsere Nutzer erst auf eine Antwort des Mailservices warten müssen, bevor sie eine Erfolgsmeldung sehen. Daher wird die sendMail-Methode via @Async als asynchron ausführbar deklariert.
Für das Beispiel nutzen wir hier keine eigene Config samt Custom-Executor-Implementierung, sondern wir nutzen den Fallback SimpleAsyncTaskExecutor von Spring Boot selbst. Damit dies auch funktioniert, müssen wir der Klasse GuestbookBackendApplication die Annotation @EnableAsync voranstellen, die uns den entsprechenden TaskExecutor bereitstellt.

@SpringBootApplication
@EnableAsync
public class GuestbookBackendApplication {
 
    public static void main(String[] args) {
	SpringApplication.run(GuestbookBackendApplication.class, args);
    }
}

Dazu deklarieren wir in unserer Domain noch in einem Interface, welche Methoden uns eine wie auch immer geartete Persistenz zur Verfügung stellen muss, um Einträge auszulesen und zu speichern:

public interface GuestbookRepository {
    List findAllOrderedByIdDesc();
    GuestbookEntry save(GuestbookEntry entry);
}

Dabei ist uns, wie erwähnt, die konkrete Implementierung unwichtig. Diese wird später in der Infrastruktur von unserer Domain entkoppelt abgebildet. Abschließend legen wir nach demselben Prinzip noch das Interface für den Mailclient fest:

public interface GuestbookMailClient {
    void sendMail(GuestbookEntry entry);
}

Als Parameter geben wir den Eintrag selbst mit, da dieser als Inhalt der Mail angezeigt werden soll.

API

Unser API findet sich im GuestbookController, der HTTP-Endpunkte fürs Hinzufügen und Anzeigen bereitstellt. Das Ganze ist auch ziemlich „straightforward“:

@RestController
@CrossOrigin(origins = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.OPTIONS, RequestMethod.PUT})
public class GuestbookController {
    private final GuestbookRepository repository;
 
    private final GuestbookMailService mailService;
 
    public GuestbookController(GuestbookRepository gbRepo, GuestbookMailService mailService) {
        this.repository = gbRepo;
        this.mailService = mailService;
    }
 
    @GetMapping(value="/guestbook",
        produces=MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<?> getEntries(Principal p) {
 
	List entries = repository.findAllOrderedByIdDesc();
 
	return ResponseEntity.ok(entries);
    }
 
    @PostMapping(value="/guestbook",
	consumes=MediaType.APPLICATION_JSON_VALUE,
	produces=MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<?> create(@RequestBody GuestbookEntry entry) {
 
	entry = repository.save(entry);
        mailService.sendMail(entry); //async call
 
        return ResponseEntity.ok(entry);
    }
}

Via GET an die URI /guestbook rufen wir eine mit dem neuesten Eintrag beginnende Liste von Einträgen über ein Repository auf. Via POST und einen Eintrag im Body erzeugen wir diesen über das Repository und verschicken asynchron eine Mail über einen mailService. Die konkreten Implementierungen injizieren wir dabei via Spring-DI-Container über Konstruktor-Injektion aus dem Package Infrastructure.

Der Principal als Parameter in getEntries ist an dieser Stelle nicht unbedingt notwendig und wird auch nicht weiter verwendet, aber wir können hier mit dem Debugger gut einmal nachschauen, welche Informationen an unseren Service geliefert werden.

falsepositive-config-intellij.png

Bild 2: Principal-Eigenschaften im Debug-Modus

In Bild 2 sehen wir, dass zum Beispiel die in Keycloak hinterlegten Rollen als Authorities mit der Spring-Security-Konvention ROLE_ mitgegeben werden. Wie wir dies erreichen, sehen wir gleich beim Anlegen der Security-Konfiguration im Infrastructure-Package.

Infrastructure

Im Package Infrastructure schließlich legen wir die konkreten Implementierungen für Probleme wie Persistenz und Kommunikation mit Third-Party-Services wie dem Mailservice an. Außerdem konfigurieren wir z.B. das tatsächlich eingesetzte Framework – in diesem Fall Spring Boot.

Anbindung von Persistenz und Mailservice

Um den Rahmen dieses Blogeintrags nicht zu sprengen, möchte ich für Einzelheiten der Persistenz-Implementierung auf das GitHub-Repository verweisen. An dieser Stelle sei nur erwähnt, dass hier ein Mapping von der Businessentität hin zur eigentlichen JPA-Entität in einem Custom-Repository stattfindet, das die Funktionalität des Spring-JPA-Repositories wrappt und das Interface aus der Domain implementiert, damit innerhalb der Domain nur wirklich die Methoden (und eventuell später die Attribute der Entität) bekannt sind, die unsere Domain zur korrekten Funktion benötigt.

Einzelheiten zum Aufruf der Mail-App werde ich dediziert im dritten Teil dieser Reihe behandeln. Im Repository bei Github sind diese Änderungen aber schon implementiert, sodass sich der Code dort leicht von den folgenden Snippets unterscheidet.

SecurityConfig

Der spannende Teil in Bezug auf Keycloak erwartet uns beim Anlegen der Security-Konfiguration für unseren Spring-Boot-Container.

Keycloak Security Config

Fangen wir also mit der entsprechenden Config-Klasse samt Konstruktor an:

@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
 
    private final KeycloakClientRequestFactory keycloakClientRequestFactory;
 
    public SecurityConfig(KeycloakClientRequestFactory keycloakClientRequestFactory) {
        this.keycloakClientRequestFactory = keycloakClientRequestFactory;
    }
}

Die Annotation @KeycloakConfiguration wrappt dabei die @Configuration-Annotation von Spring Boot und registriert die Klasse KeycloakSecurityComponents.class als zu scannendes BasePackage. In früheren Versionen des Keycloak-Adapters musste dies noch über die entsprechende Annotation @ComponentScan(basePackageClasses = KeycloakSecurityComponents.class) getan werden.

Durch diese Annotation und das Erweitern des KeycloakWebSecurityConfigurerAdapters ist es uns jetzt möglich, die vom Adapter bereitgestellte KeycloakClientRequestFactory via Dependency Injection in die Config-Klasse zu injizieren. Diese verarbeitet ein- und ausgehende HTTP-Requests und integriert für Keycloak die Prüfung auf Authorization-Header und das Vorhandensein eines Principals bzw. einer validen Spring Security Authentication zu einem Principal. Dies ist u.A. auch wichtige Grundlage zur Nutzung des KeycloakRestTemplates, auf das wir in Teil 3 dieser Serie noch näher eingehen werden.

ACHTUNG! Zumindest bei Intellij IDEA erzeugt dieses Vorgehen, wie in Bild 3 zu sehen, einen False Positive bei der Konstruktor-Injektion. Diesen ignorieren wir geflissentlich. Trust me, i’m an engineer es funktioniert, ich hab’s auch gemacht 😉

falsepositive-config.png

Bild 3: False Positive von Intellij

Mapping von Rollen auf Authorities

Als nächstes sorgen wir dafür, dass unsere in Keycloak angelegten Rollen, wie in Spring Security gewünscht, mit dem Präfix ROLE_ versehen werden. Dazu ändern wir zunächst den AuthenticationProvider, der die Authentication Requests verwaltet, auf den von Keycloak gestellten KeycloakAuthenticationProvider.
Für diesen Custom-AuthenticationProvider können wir nun genau spezifizieren, wie er eingehende Requests zu behandeln hat. Das entsprechende Mapping der Keycloak-Rollen in für Spring Security verständliche Authorities mit dem Präfix ROLE_ erreichen wir nun, indem wir den von Spring zur Verfügung gestellten SimpleAuthorityMapper als GrantedAuthoritiesMapper setzen.

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    KeycloakAuthenticationProvider keyCloakAuthProvider = keycloakAuthenticationProvider();
    keyCloakAuthProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
 
    auth.authenticationProvider(keyCloakAuthProvider);
}

Der Vorteil bei dieser Vorgehensweise ist, dass wir dadurch die Freiheit haben, weiterhin unsere Rollen frei in Keycloak zu pflegen und diese trotzdem ohne Umstände in Spring Security nutzen können.

Nutzung von Spring Boot Properties

Keycloak nutzt standardmäßig eine keycloak.json-Datei, die umgebungsabhängige Parameter setzt. Wir wollen aber unsere Konfiguration an einem Ort haben und eigentlich gar nicht so genau wissen, was Keycloak da intern macht, also ab damit in die spring.properties:

@Bean
public KeycloakConfigResolver KeyCloakConfigResolver(){
    return new KeycloakSpringBootConfigResolver();
}

Um dies zu erreichen, legen wir in der deklarierten Bean KeyCloakConfigResolver fest, dass wir den KeycloakSpringBootConfigResolver nutzen wollen, der uns genau dieses Mapping abnimmt.

Danach legen wir auch direkt im resources-Ordner eine entsprechende application-properties (oder yaml, ganz, wie ihr wollt) an und füllen die Datei mit Inhalt:

server.port=8090
 
#keycloak
keycloak.auth-server-url=http://localhost:8080/auth
keycloak.realm=springboot-example
keycloak.bearer-only=true
keycloak.resource=guestbook-backend-app
keycloak.principal-attribute=preferred_username
keycloak.cors=true
 
#ugly implementation of endpoint matchers when no spring security integration is set.
#keycloak.security-constraints[0].authRoles[0]=user
#keycloak.security-constraints[0].securityCollections[0].patterns[0]=/guestbook*

Unser Backendservice ist also auf Port 8090 erreichbar, Realm und Client sind spezifiziert, er nutzt CORS und ist so konfiguriert, dass er keinen eigenen Login zur Verfügung stellt, sondern nur mit Bearer Tokens arbeitet.

Interessant ist hier die Einstellung keycloak.principal-attribute – dadurch, dass wir hier preferred_username als Wert setzen, wird der Benutzername des Keycloak-Accounts als Primärattribut des Principals gesetzt (siehe Bild 2), standardmäßig wäre dies die Keycloak-UUID des Accounts. Es gibt hier noch viele weitere Möglichkeiten und Einstellungen, zu finden in der Dokumentation.

Daneben habe ich hier der Vollständigkeit halber als Kommentar noch die Implementierung der Authentifizierungsprüfung ohne Spring-Security-Adapter gelistet. Zugegebenermaßen nicht unbedingt schön – aber wir sind ja gerade dabei, das zu ändern.

Session-Management

Die gute Nachricht vorab: Wir wollen keine Sessions. HTTP is stateless, and so are we!
Aber um dies zu erreichen, müssen wir auch hier etwas am Standard-Prozedere von Spring Security ändern, und zwar die SessionAuthenticationStrategy. Diese setzen wir wieder innerhalb eines @Bean auf die NullAuthenticatedSessionStrategy:

@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
    return new NullAuthenticatedSessionStrategy();
}

Außerdem müssen wir dafür in der Security Filter Chain von Spring Security Einstellungen vornehmen, aber vorher wollen wir noch zwei letzte Kleinigkeiten erledigen, um unsere Konfiguration abzuschließen.
Dazu möchte ich hier die Dokumentation zitieren:

Spring Boot attempts to eagerly register filter beans with the web application context. Therefore, when running the Keycloak Spring Security adapter in a Spring Boot environment, it may be necessary to add two FilterRegistrationBeans to your security configuration to prevent the Keycloak filters from being registered twice.

Um also die doppelte Registrierung der Keycloak-Filter im Context zu unterbinden, fügen wir noch folgende Beans zur Config hinzu:

@Bean
public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(
        KeycloakAuthenticationProcessingFilter filter) {
    FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
    registrationBean.setEnabled(false);
    return registrationBean;
}
 
@Bean
public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(
        KeycloakPreAuthActionsFilter filter) {
    FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
    registrationBean.setEnabled(false);
    return registrationBean;
}

Die Security Filter Chain

Nun haben wir viel konfiguriert – fehlt nur noch, dass wir unsere Endpunkte auch wirklich schützen. Schönerweise können wir dies nun in altbekannter Manier via Security Filter Chain in der Configure-Methode von Spring Security tun:

@Override
protected void configure(HttpSecurity http) throws Exception
{
    super.configure(http);
    http.cors()
        .and()
        .csrf()
        .disable()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .sessionAuthenticationStrategy(sessionAuthenticationStrategy())
        .and()
        .authorizeRequests()
        .antMatchers("/guestbook*").hasRole("user")
        .anyRequest().permitAll();
}

Hier legen wir zuerst die CORS-Unterstützung fest, um die @CrossOrigin-Annotation in unserem Controller nutzen zu können. Dann deaktivieren wir der Einfachheit halber für dieses Szenario das CSRF-Token. Klar, dass der Produktiveinsatz dieser Lösung von mir nicht empfohlen wird.
Nun legen wir die sessionCreationPolicy auf STATELESS fest und setzen das vorhin angelegte sessionAuthenticationStrategy-Bean als Strategie. Dadurch werden jetzt wirklich keinerlei Request-Caches o.Ä. mehr genutzt und jeder Request muss authentifiziert werden. Abschließend legen wir noch via antmatcher fest, dass Accounts die Rolle User besitzen müssen, um unseren Endpunkt aufrufen zu dürfen. Das Ganze sieht so bekannt aus, wie es auch aussehen soll – wir sind nämlich fertig und haben Keycloak erfolgreich in Spring Security integriert.

Fazit

In diesem Teil der Artikelserie haben wir Keycloak via Spring-Security-Adapter in Spring Security integriert und dadurch erreichen können, dass wir „wie immer“ unsere Security Filter Chain aufbauen können. Führen wir den Code aus dem Repo aus, merken wir, dass unsere Backend-App noch eine Exception wirft, da wir noch keinen Mailservice implementiert haben. Das wollen wir im nächsten und erst einmal letzten Teil dieser Serie ändern.

Code: GitHub
Weitere Artikel dieser Serie: Teil 1, Teil3 (TODO)

Dominik arbeitet als erfahrener Software-Entwickler und Consultant im Bereich verteilte Systeme. Er unterstützt seine Kunden in der Entwicklung verteilter Systeme, sei es bei der Entwicklung von RESTful APIs und Webapplikationen oder der Entwicklung von Microservice- und Big-Data-Architekturen.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.