Einstieg in Spring Boot, Teil 22 Verriegelt und verrammelt – Security mit Spring Boot

Autor / Redakteur: Dr. Dirk Koller / Stephan Augsten

Das Thema Sicherheit steht bei vielen Unternehmen ganz oben auf der Agenda. Mit der grundlegenden Absicherung von Spring-Projekten befassen wir uns im vorerst letzten Beitrag dieser Serie.

Firmen zum Thema

Aufruf eines REST-Service mit Postman und Authentication-Header.
Aufruf eines REST-Service mit Postman und Authentication-Header.
(Bild: Koller / Spring.io)

Die Gefahr, dass durch einen Angriff Daten entwendet oder verschlüsselt werden, beschert vielen Verantwortlichen schlaflose Nächte. Auch Spring Boot kann dem Thema Brisanz und Komplexität nicht nehmen, aber zumindest eine Grundabsicherung eines Projekts ist schnell realisiert, sie erfolgt einfach durch Zufügen des Starters spring-boot-starter-security in POM.xml:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Die Sicherheitseinstellungen entsprechen danach vermutlich noch nicht den eigenen Vorstellungen, aber eine MVC-Anwendung, wie sie in vorgehenden Teilen dieser Reihe besprochen wurde, ist zumindest rudimentär abgesichert. Das zeigt der Zugriff auf eine beliebige Seite der Webanwendung.

Login-Seite nach Einbinden des Starters in eine Webanwendung.
Login-Seite nach Einbinden des Starters in eine Webanwendung.
(Bild: Koller / Spring.io)

Anstelle der erwarteten HTML-Seite erscheint nach dem Einbinden des Security-Starter eine Login-Seite unter /login. Spring leistet hier im Hintergrund offensichtlich ganze Arbeit. Der für den Zutritt erforderliche Username ist user, das Passwort findet sich in den Log-Ausgaben in der Konsole:

Using generated security password: 83f9365e-91a1-4d5a-b534-6bbac903a0e8

Nach Eingabe dieser Daten landet man dann auf der ursprünglich gewünschten Seite. Ähnlich funktioniert das für einen REST-Service. Anstelle der erwarteten JSON-Antwort erhält man nach Zufügen des Security-Starter eine 401/Unauthorized-Meldung beim Aufruf eines beliebigen Endpoints:

{
   "timestamp": "08:40:33.130+00:00",
   "status": 401,
   "error": "Unauthorized",
   "path": "/customers"
}

Aufruf eines REST-Service mit Postman und Authentication-Header.
Aufruf eines REST-Service mit Postman und Authentication-Header.
(Bild: Koller / Spring.io)

Erst durch Angabe von Username und Passwort in Form eines Basic Authentication-Header lässt sich der Service (im Beispiel mit Postman) wieder aufrufen.

Authentifizierung

Das Passwort in der Konsole ändert sich bei jedem Neustart und ist offensichtlich nicht für den produktiven Einsatz gedacht. Das Anpassen der Security-Konfiguration an die eigenen Erfordernisse erfolgt in einem WebSecurityConfigurer, den man am einfachsten durch Erben von der Klasse WebSecurityConfigurerAdapter erstellt. In der Konfigurationsklasse wie sie im folgenden Beispiel wiedergegeben ist, wird die Security durch verschiedene Methoden beschrieben.

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
   @Autowired
   public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
      auth.inMemoryAuthentication()
      .withUser("user").password("{noop}password").roles("USER")
      .and()
      .withUser("admin").password("{noop}password").roles("ADMIN");
   }
}

Im Beispiel werden mithilfe des AuthenticationManagerBuilder die beiden Benutzer user und admin, beide mit dem Passwort password und den Rollen USER und ADMIN definiert. Auf eine Verschlüsselung des Passworts wird in diesem einfachen Beispiel verzichtet (noop steht für NoOpPasswordEncoder).

Sowohl die Webanwendung als auch der REST-Endpoint lassen sich nach dem Neustart mit den vergebenen Accountdaten aufrufen, die Ausgabe mit dem von Spring generierten Passwort in der Konsole entfällt. In der Praxis möchte man das eingegebene Passwort vielleicht eher mit einem Wert aus einer Datenbank vergleichen.

Kein Problem: Anstelle der Methode inMemoryAuthentication() lässt sich das durch Aufruf von jdbcAuthentication() von AuthenticationManagerBuilder realisieren. Das folgende Codestück zeigt ein Beispiel:

auth.jdbcAuthentication()
   .dataSource(dataSource)
   .withDefaultSchema()
   .withUser(User.withUsername("user").password(passwordEncoder().encode("pass")).roles("USER"));

Authorisierung

Nachdem der Benutzer nun authentifiziert ist (wer ist es?) folgt die Autorisierung (was darf er?). Dafür wird die Methode configure() überschrieben. Auch hier wird ein Objekt, diesmal vom Typ HttpSecurity, durch Aufruf verschiedener Methoden konfiguriert. Jede davon gibt einen Configurer wie zum Beispiel HttpBasicConfigurer oder FormLoginConfigurer zurück. Die Verknüpfung erfolgt durch die and()-Methode:

@Override
protected void configure(HttpSecurity http) throws Exception {
http
   .authorizeRequests().antMatchers("/adminspace").access("hasRole('ADMIN')")
      .and()
   .authorizeRequests().anyRequest().authenticated()
      .and()
   .formLogin() // .loginPage("/login")
      .and()
   .httpBasic();
}

Im Beispiel oben wird die Authentifizierung über die Standard-Login-Seite (Aufruf von formLogin()) sowie über Basic Authentication (Aufruf von httpBasic()) ermöglicht. Alle Requests müssen authentifiziert sein und die URL /adminspace ist nur für User mit der Rolle ADMIN zugänglich. Eine eigene Login-Seite lässt sich durch Aufruf von loginPage() konfigurieren, im Code ist das als Kommentar angedeutet.

Das Formulieren der Einschränkungen ist alles andere als selbsterklärend, aber in der Klasse HttpSecurity finden sich zahlreiche Konfigurationsbeispiele in Form von Javadoc-Kommentaren.

Methoden Level Security

Die oben geschilderte Absicherung mit Hilfe des HttpSecurity-Objekts basiert auf Servlet-Filtern, eingehende Requests werden überprüft und gegebenenfalls abgewiesen. Neben diesem Mechanismus existiert ein zweiter, der auf AOP-Proxies setzt und ohne Web-Technologien auskommt.

Mit Hilfe dieser als Methoden-Level-Security bezeichneten Variante lassen sich beispielsweise Methoden in Services absichern. Dazu wird vor die zu sichernde Methode die Annotation PreAuthorize gesetzt, und als value-Attribut ein Spring Expression Language (SpEL)-Ausdruck angegeben. Die Methode in folgendem Beispiel lässt sich wieder nur mit der ADMIN-Rolle aufrufen:

@Service
public class AnyService {
   @PreAuthorize(value = "hasRole('ADMIN')")
   public void anyMethod() {
      ...
   }
}

Damit das funktioniert, ist noch die Annotation @EnableGlobalMethodSecurity_ in einer Konfiguration unterzubringen:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
   ...
}

Im Beispiel werden mit dem Attribute prePostEnabled die Pre- und Post-Annotationen aktiviert. Neben den Pre- und PostAuthorize-Annotationen existieren noch die Pre- und PostFilter-Annotationen. Auch sie funktionieren mit SpEL und werden benutzt, um Collections oder Arrays abhängig vom Ausdruck zu filtern, und damit Elemente vor der Rückgabe zu entfernen. Die alternativen Attribute securedEnabled und jsr250Enabled betreffen die Annotationen @Secured_ und @RoleAllowed_ – beide arbeiten nicht mit SpEL-Ausdrücken und validieren nur gegen Rollen.

Das komplexe Thema Security in Spring ist mit diesem Beitrag nur angerissen, es existieren ganze Bücher dazu (zum Beispiel Spring Security in Action von Laurențiu Spilcă). Mehr über die zugrundeliegenden Konzepte erfährt man auch unter Spring Security Architecture.

(ID:47710477)