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 {}
}

Gradle Update unter Eclipse

Hat man Änderungen an dem Buildskript vorgenommen, dann muss man dieses manuell Eclipse mitteilen. Dazu wird im Projekt View wird ein Gradle Projekt über Gradle –> Update Gradle manuell geupdatet. D.h. das Modell wird neu geladen und somit Änderungen an den Dependencies in Eclipse propagiert.

In den Einstellungen (Preferences –> Gradle –> Automatic Project synchronization) kann der Vorgang automatisch bei Änderungen am Buildskript angestoßen werden.