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:
1... 2<dependency> 3 <groupId>org.keycloak</groupId> 4 <artifactId>keycloak-spring-security-adapter</artifactId> 5 <version>3.2.1.Final</version> 6</dependency> 7<dependency> 8 <groupId>org.keycloak</groupId> 9 <artifactId>keycloak-spring-boot-starter</artifactId> 10 <version>3.2.1.Final</version> 11</dependency> 12...
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.
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.
1public class GuestbookEntry {
2
3 private Long id;
4
5 private String title;
6
7 private String comment;
8
9 private String commenter;
10
11 @JsonFormat(pattern = "dd.MM.yyyy, HH:mm:ss")
12 private Date date = new Date();
13
14 //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:
1@Service
2public class GuestbookMailService {
3
4 private final GuestbookMailClient mailClient;
5
6 public GuestbookMailService(GuestbookMailClient mailClient) {
7 this.mailClient = mailClient;
8 }
9
10 @Async
11 public void sendMail(final GuestbookEntry entry) {
12
13 System.out.println("Sending Mail...");
14 mailClient.sendMail(entry);
15 System.out.println("Successfully sent Mail!");
16
17 }
18}
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.
1@SpringBootApplication
2@EnableAsync
3public class GuestbookBackendApplication {
4
5 public static void main(String[] args) {
6 SpringApplication.run(GuestbookBackendApplication.class, args);
7 }
8}
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:
1public interface GuestbookRepository {
2 List findAllOrderedByIdDesc();
3 GuestbookEntry save(GuestbookEntry entry);
4}
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:
1public interface GuestbookMailClient {
2 void sendMail(GuestbookEntry entry);
3}
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“:
1@RestController
2@CrossOrigin(origins = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.OPTIONS, RequestMethod.PUT})
3public class GuestbookController {
4 private final GuestbookRepository repository;
5
6 private final GuestbookMailService mailService;
7
8 public GuestbookController(GuestbookRepository gbRepo, GuestbookMailService mailService) {
9 this.repository = gbRepo;
10 this.mailService = mailService;
11 }
12
13 @GetMapping(value="/guestbook",
14 produces=MediaType.APPLICATION_JSON_VALUE)
15 public ResponseEntity<?> getEntries(Principal p) {
16
17 List entries = repository.findAllOrderedByIdDesc();
18
19 return ResponseEntity.ok(entries);
20 }
21
22 @PostMapping(value="/guestbook",
23 consumes=MediaType.APPLICATION_JSON_VALUE,
24 produces=MediaType.APPLICATION_JSON_VALUE)
25 public ResponseEntity<?> create(@RequestBody GuestbookEntry entry) {
26
27 entry = repository.save(entry);
28 mailService.sendMail(entry); //async call
29
30 return ResponseEntity.ok(entry);
31 }
32}
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.
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:
1@KeycloakConfiguration
2public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
3
4 private final KeycloakClientRequestFactory keycloakClientRequestFactory;
5
6 public SecurityConfig(KeycloakClientRequestFactory keycloakClientRequestFactory) {
7 this.keycloakClientRequestFactory = keycloakClientRequestFactory;
8 }
9}
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 😉
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.
1@Autowired
2public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
3 KeycloakAuthenticationProvider keyCloakAuthProvider = keycloakAuthenticationProvider();
4 keyCloakAuthProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
5
6 auth.authenticationProvider(keyCloakAuthProvider);
7}
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:
1@Bean
2public KeycloakConfigResolver KeyCloakConfigResolver(){
3 return new KeycloakSpringBootConfigResolver();
4}
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:
1server.port=8090 2 3#keycloak 4keycloak.auth-server-url=http://localhost:8080/auth 5keycloak.realm=springboot-example 6keycloak.bearer-only=true 7keycloak.resource=guestbook-backend-app 8keycloak.principal-attribute=preferred_username 9keycloak.cors=true 10 11#ugly implementation of endpoint matchers when no spring security integration is set. 12#keycloak.security-constraints[0].authRoles[0]=user 13#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:
1@Bean
2@Override
3protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
4 return new NullAuthenticatedSessionStrategy();
5}
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:
1@Bean
2public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(
3 KeycloakAuthenticationProcessingFilter filter) {
4 FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
5 registrationBean.setEnabled(false);
6 return registrationBean;
7}
8
9@Bean
10public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(
11 KeycloakPreAuthActionsFilter filter) {
12 FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
13 registrationBean.setEnabled(false);
14 return registrationBean;
15}
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:
1@Override
2protected void configure(HttpSecurity http) throws Exception
3{
4 super.configure(http);
5 http.cors()
6 .and()
7 .csrf()
8 .disable()
9 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
10 .sessionAuthenticationStrategy(sessionAuthenticationStrategy())
11 .and()
12 .authorizeRequests()
13 .antMatchers("/guestbook*").hasRole("user")
14 .anyRequest().permitAll();
15}
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 , | Teil 3
Weitere Beiträge
von Dominik Guhr
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Dominik Guhr
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.