Hier in diesem Betrag gehe ich auf ein paar grundlegende Sicherheitsmaßnahmen ein. Ziel ist es einige wichtige Maßnahmen zum Schutz der Daten vorzustellen und wie diese mit Spring Security genutzt werden können.

CSRF

Cross Site Request Forgery kann man frei mit Seitenübergreifende Anfragenfälschung übersetzen. Um zu verhindern das ein Angreifer Anfragen an die Anwendung stellt, muss man sicherstellen dass der Link auf den der Benutzer geklickt hat nicht untergeschoben worden ist. Dieses kann man recht einfach erreichen, in dem man ein Geheimnis in der Webseite unterbringt. Dieses Geheimnis, es handelt sich hierbei um das sogenannte CSRF-Token, kennt nur die Anwendung (Server) und der Anwender (Client). Das Token hat eine kurz Lebensdauer und wird als hidden input field mit ausgegeben. Hier am Beispiel in Verwendung mit Thymeleaf Templating Engine.

<input
  type="hidden"
  th:name="${_csrf.parameterName}"
  th:value="${_csrf.token}" />

Es ist also nicht sehr kompliziert, aber dennoch eine Wirkungsvolle Waffe gegen mögliche Angreifer. Alle Anwendungen sollten also möglichst CSRF verhindern. Weitere Sicherungsmaßnahmen befinden sich gut beschrieben in der Referenzdokumentation von Spring Boot Security.

Hinweis: Ab Thymeleaf 2.1 und gesetzter @EnableWebSecurity fügt Thymeleaf dieses automatisch ein.

CSRF deaktivieren

Per default ist CSRF aktiviert. Das heißt das Seiten die von der Anwendung ausgeliefert werden, auch das Token beinhalten müssen, da sonst zu recht ein 403 Fehler angezeigt wird. In dem WebSecurityConfigurerAdapter kann man CSRF deaktivieren.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .csrf()
            .disable() // TODO remove and secure template with CSRF token
    ...
}

Die Deaktivierung, auch wenn es nur in der Entwicklung ist, ist nicht zu empfehlen. Sicherheit in Anwendungen sollten von Anfang an integraler Bestandteil der Anwendung sein und nicht im Nachhinein übergestülpt werden. Daher rate ich von dieser Vorgehensweise hier ausdrücklich ab.

Dependency

Es muss nur der Starter spring-boot-starter-security eingebunden werden. Dieses ist die einzige Abhängigkeit, um die Anwendung mit Spring Security abzusichern.

dependencies {
    // Spring Boot
    ...    
    implementation('org.springframework.boot:spring-boot-starter-security')
    ...
}

Sicherung auf Methoden Level

Mit welchen Einstellungen lässt sich die Methodensicherung konfigurieren?

Grundsätzlich erfolgt die Steuerung über Annotationen. Welche Annotationen Wirkung haben, hängt davon ab welche Sicherungsmaßnahmen aktiviert sind. In der @EnableGlobalMethodeSecurity können als Paramter folgende Annotationen gesteuert werden:

  • securedEnabled

Methoden oder Klassen können nun mit @Secure() annotiert werden.

  • jsr250Enaabled Die Annotaion jsr250Enabled ist das Equivalent zur Spring Annotation Secured

  • order

  • prePostEnabled

Wenn prePostEnabled wahr ist, dann lassen sich Methoden vor Aufruf oder bevor die Rückgabewerte übergeben werden prüfen.

Beispiel

Wie sieht nun eine typische Konfiguration aus? Hier nachfolgend eine mit @Configuration annotierte Konfiguration, die secured ermöglicht und prePost aktiviert.

@Configuration
@EnableGlobalMethodSecurity(securedEnabled=true, prePostEnabled=true)
public class EnableMethodSecurity extends GlobalMethodSecurityConfiguration {

}

Kurztipp für die Eingabe

Legt man die Klasse mit CTRL + n –> class neu an, dann kann man direkt GMSC + STRG + SPACE eingeben und Eclipse expandiert dann zur gewünschten Klasse GlobalMethodSecurityConfiguration. Man muss also nur die Anfangsbuchstaben wählen und dann mit STRG + SPACE die Autovervollständigung akitivieren.

Den Ausdruck hasPermission() implementieren

Neben dem Standard hasRole(“”) kann man in Spring auch eigene Ausdrücke implementieren. Hier zeige ich an dem Beispiel, wie man die hasPermissions() implementieren kann.

Dazu muss die Klasse das Interface PermissionEvaluator implementieren. Zunächst das Grundgerüst:

@Override
public boolean hasPermission(Authentication authentication,
                             Object targetDomainObject,
                             Object permissionObject) {
    if(authentication==null ||
            targetDomainObject==null ||
            !(permissionObject instanceof String)) {

        log.info("Permission denied");

        return false;
    }

    String targetType = targetDomainObject.getClass()
                            .getSimpleName()
                            .toUpperCase();
    String permission = permissionObject
                            .toString()
                            .toUpperCase();

    return hasPrivilege(authentication, targetType, permission);
}

@Override
public boolean hasPermission(Authentication authentication,
                             Serializable targetId,
                             String targetType,
                             Object permissionObject) {
    if ((authentication == null) || 
        (targetType == null) || 
        !(permissionObject instanceof String)) {
        return false;
    }

    String permission = permissionObject
                            .toString()
                            .toUpperCase();

    return hasPrivilege(authentication, targetType, permission);
}

/**
    * Check privilege on given constraints
    * 
    * @param authentication
    * @param targetType
    * @param permission
    * 
    * @return true if access is granted, otherwise false
    */
private boolean hasPrivilege(Authentication authentication,
                             String targetType,
                             String permission) {
    for (final GrantedAuthority grantedAuth : authentication.getAuthorities()) {
        if (grantedAuth.getAuthority().startsWith(targetType)) {
            if (grantedAuth.getAuthority().contains(permission)) {
                return true;
            }
        }
    }
    return false;
}

Methoden absichern

@Controller
public class CustomerController {
    /**
     * Show table with all Customers. This endpoint is protected
     * by hasPermission expression evaluation in PreAuthorize() annotation.
     */
    @GetMapping("/allcustomersHP")
    @PreAuthorize("hasPermission(#model, 'String', 'xxx')")
    public String allCustomersHP(Model model) {
        model.addAttribute("customers", customerService.findAll());
        return "customerListing";
    }

ACHUTNG: Damit @PreAuthorize ausgeführt und somit auch der Ausdruck hasPermission() evaluiert wird, muss in der Konfiguration prePostEnabled=true gesetzt werden. Siehe oben.

Benutzerdefinierte Ausdrücke für die Validierung

Es lassen sich nicht immer alle Umgebungen auf die hasPermission() abbilden, was dazu führen kann das die Funktion, die ja nur die Permission klären soll, auch andere Zwecke erfüllt. Diese Zweckentfremdung führt früher oder später zu Problemen.

Gegeben sei ein größerer Betrieb mit vielen Organisationseinheiten. Dann macht es Sinn zum Beispiel den Zugriff auf die Umsatzzahlen, einer bestimmten Gruppe von Mitarbeiten zu gewähren. Hier wäre es die Controllingabteilung.

Anstelle mit hasPermission auf isController oder ähnliches zu Prüfen, kann man auch gleich die Organisationeinheiten mit in die Benutzerdaten integrieren und z.B. mit einer Funktion isMemberOf(String organization) prüfen. Dazu muss eine Klasse den SecurityExpressionRoot erweitern. Dieses Thema werde ich in einem weiterem Beitrag ausführlich behandeln.

Sicherheitsmaßnahmen komplett deaktivieren

Wenn sich Spring Security im Classpath befindet, dann werden alle Endpoints (bis auf 2 Actuator /health, /info) gesichert und sind ohne weiteres Zutun nicht mehr erreichbar. Und es ist auch gut so, dass das Spring Team dieses konservative Standardverhalten gewählt hat.

Möchte man zum lokalen Testen nicht jedesmal die Dependencies anpassen, so kann man zum Beispiel auch eine 2. Konfiguration anlegen, die nur bei einem bestimmten Profil aktiviert wird. Überschreibt man die beiden Methodensignaturen configure(final AuthenticationManagerBuilder) und configure(final HttpSecurity), dann hat man den Effekt, dass Spring Security nicht im Classpath vorhanden sei.

@Profile("localdebug")
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(final AuthenticationManagerBuilder auth) throws Exception {}

  @Override
  protected void configure(final HttpSecurity http) throws Exception {}
}

PasswordEncoder

Ohne weiteres Zutun legt Spring Security die Passwörter unverschlüssel ab. Das sollte heute aber nicht mehr der Fall sein, da zum Beispiel wenn die Passwörter der Benutzer in einer Datenbank gespeichert werden diese bei einem Einbruch als Plaintext vorliegen. Daher werden (sollten / es gibt immer wieder Beispiele die das Gegenteil beweisen) die Passwörter verschlüsselt abgelegt. Dieses kann zum Beispiel ein Hash des Passwortes sein.

Um Spring Security flexible zu gestalten, kann man zur Laufzeit einen PasswordEncoder setzen. Die Einstellungen die die Sicherheit einer Spring Anwendung betreffen, werden in einer WebSecurityConfigurerAdapter erweiterten Konfiguration vorgenommen. Hierfür muss die Einstellung für die Authentifikation geändert werden. Dafür die ist die Methode passwordEncoder() verantwortlich.

Als ein guter Kandidat für die verschlüsselte Ablage ist BCrypt. In Spring Security implementiert die Klasse BCryptPasswordEncoder den BCrypt Algorithmus.

Damit dieses per DI injeziert werden kann, benötigen wir eine Bean die uns den BCryptPasswordEncoder liefert.

@Bean PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

Jetzt kann ein PasswordEncoder per @Autowired zur Laufzeit gesetzt werden und als Parameter für passwordEncoder() Funktion dienen.

@Autowired
private PasswordEncoder passwordEncoder;

Grundgerüst einer Anwendung

Dependency

Als Abhängigkeit ist natürlich spring-boot-starter-security einzufügen.

dependencies {
    // Spring Boot
    ...
    implementation('org.springframework.boot:spring-boot-starter-security')
    ... 

Konfiguration WebSecurityConfigurerAdapter

Hier ein sehr simples Beispiel mit einer in Memory Benutzerauthentifizierung. Ich glaube jeder hat schon einmal die verwendeten Benutzernamen und Passwörter gesehen… 🙂

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Bean PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .passwordEncoder(passwordEncoder)
            .withUser("user")
            .password("$2a$10$9z954e6ETmwauiiBbwCNbOwU90UnUrh9hpRUWwNmWUx6aFic2nE46") // user
            .roles("USER")
                .and()
            .withUser("admin")
            .password("$2a$10$Tljz0HJByMV97Y92B65QBu0TckqbJDlT1kgmibMijhbMtpHPORanK") // admin
            .roles("ADMIN");
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
                .antMatchers("/admin/**").hasAnyRole("ADMIN")
                .anyRequest().hasAnyRole("USER").and()
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/allcustomers")
                .permitAll();
    }
}