Sichere Web-Applikationen mit Vaadin und Spring Boot

Verlässliche Sicherheit mit Spring Security So gut wie alle Web Applikation Frameworks bringen eigene Sicherheitsfunktionen und ensprechende Schnittstellen zu externen

blog-post-img

Verlässliche Sicherheit mit Spring Security

So gut wie alle Web Applikation Frameworks bringen eigene Sicherheitsfunktionen und ensprechende Schnittstellen zu externen Sicherheitslösungen mit. Als Java-basiertes Full-Stack Framework ist Vaadin kompatibel zu den beliebtesten Java Sicherheitslösungen. Spring Security, Apache Shiro und JAAS währen hier nennenswert. 

Wenn man mit Vaadin und Spring Boot Web Applikationen baut, dann wäre das Spring Security Modul des Spring Frameworks das Mittel der Wahl. Dieses lässt sich nämlich ganz einfach mit Vaadin Flows Security Helpers verbinden. Diese ermöglichen eine view-basierte Zugriffskontrolle, ohne dass man Spring Security gross konfigurieren muss. 

So kann jeder View fein säuberlich getrennt und mit eigenen Richtlinien abgesichert werden. Verwendet werden hier die @DenyAll, @PermitAll, @RolesAllowed, und @AnonymousAllowed Annotationen, welche auf den jeweiligen View Klassen angewandt werden.
Benötigt werden hierfür in dem Projekt eine Log-in View, eine Möglichkeit für den Anwender um wieder auszuloggen, die Spring Security Dependencies, eine Klasse für die Konfiguration der Sicherheit (welche VaadinWebSecurity erweitert) und eine der folgenden Annotationen auf jeder Ihrer View Klassen: @PermitAll, @RolesAllowed, @AnonymousAllowed.

Natürlich mit Log-in View

Die Log-in View ist die erste Bastion unserer Web Applikation. Hier findet die erste Authentifizierung unserer Anwender statt. Man kann hier auf Vaadins Log-in Form Komponente zurückgreifen, oder eine eigene Log-in View implementieren. Hier kann man dann auch mit modernen Authentifizierungsmethoden wie die Zwei-Faktor Authentifizierung per Google Authenticator Library arbeiten. Das folgende Listing zeigt wie eine simple Log-In View mit der Vaadin Komponente und i18next implementiert werden kann.

package ch.jtaf.ui.view;

import ch.jtaf.configuration.security.SecurityContext;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.login.LoginI18n;
import com.vaadin.flow.component.login.LoginOverlay;
import com.vaadin.flow.router.AfterNavigationEvent;
import com.vaadin.flow.router.AfterNavigationObserver;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;

import java.io.Serial;

@Route
@PageTitle("JTAF - Login")
public class LoginView extends LoginOverlay implements AfterNavigationObserver, BeforeEnterObserver {

    @Serial
    private static final long serialVersionUID = 1L;

    public LoginView() {
        var i18n = LoginI18n.createDefault();

        i18n.setHeader(new LoginI18n.Header());
        i18n.getHeader().setTitle("JTAF");
        i18n.getHeader().setDescription("Track and Field");
        i18n.setAdditionalInformation(null);

        i18n.setForm(new LoginI18n.Form());
        i18n.getForm().setSubmit(getTranslation("Sign.in"));
        i18n.getForm().setTitle(getTranslation("Sign.in"));
        i18n.getForm().setUsername(getTranslation("Email"));
        i18n.getForm().setPassword(getTranslation("Password"));

        i18n.getErrorMessage().setTitle(getTranslation("Auth.ErrorTitle"));
        i18n.getErrorMessage().setMessage(getTranslation("Auth.ErrorMessage"));

        setI18n(i18n);

        setForgotPasswordButtonVisible(false);

        setAction("login");
       UI.getCurrent().getPage().executeJs("document.getElementById('vaadinLoginUsername').focus();");
    }

    @Override
    public void beforeEnter(BeforeEnterEvent event) {
        if (SecurityContext.isUserLoggedIn()) {
            event.forwardTo(DashboardView.class);
        } else {
            setOpened(true);
        }
    }

    @Override
    public void afterNavigation(AfterNavigationEvent event) {
        setError(event.getLocation().getQueryParameters().getParameters().containsKey("error"));
    }

}

Spring Boot Security Dependencies

Um das Spring Security Modul nutzen zu können muss man seinem Projekt die Spring Boot Starter Security Dependency hinzufügen, was man mit der groupID org.springframework.boot und der artifactID spring-boot-starter-security erledigt:

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

Log-out

Unsere Anwender sollen nicht nur per time-out ausgeloggt werden; dies sollte Ihnen auch manuell möglich sein. Hierzu kann man einen simplen Log-Out Button in dem Header erstellen:

public class MainLayout extends AppLayout {

    private SecurityService securityService;

    public MainLayout(@Autowired SecurityService securityService) {
        this.securityService = securityService;

        H1 logo = new H1("Unsere Web Applikation");
        logo.addClassName("logo");
        HorizontalLayout header;
        if (securityService.getAuthenticatedUser() != null) {
            Button logout = new Button("Ausloggen", click ->
                    securityService.logout());
            header = new HorizontalLayout(logo, logout);
        } else {
            header = new HorizontalLayout(logo);
        }

        addToNavbar(header);
    }
}

Dieser Log-Out Button wird dann mit der Funktionalität der Spring Security API bestückt; hierzu dient diese SecurityService Klasse:

@Component
public class SecurityService {

    private static final String LOGOUT_SUCCESS_URL = "/";

    public UserDetails getAuthenticatedUser() {
        SecurityContext context = SecurityContextHolder.getContext();
        Object principal = context.getAuthentication().getPrincipal();
        if (principal instanceof UserDetails) {
            return (UserDetails) context.getAuthentication().getPrincipal();
        }
        // Anonymous or no authentication.
        return null;
    }

    public void logout() {
        UI.getCurrent().getPage().setLocation(LOGOUT_SUCCESS_URL);
        SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
        logoutHandler.logout(
                VaadinServletRequest.getCurrent().getHttpServletRequest(), null,
                null);
    }

Die Sicherheitskonfiguration

Wir müssen die VaadinWebSecurity Klasse erweitern. Hierzu erstellen wir die Klasse SicherheitsKonfiguration:

@EnableWebSecurity 
@Configuration
public class SicherheitsKonfiguration
                extends VaadinWebSecurity { 

    @Override
    protected void configure(HttpSecurity http) throws Exception {
     
        http.authorizeRequests().antMatchers("/public/**")
            .permitAll();

        super.configure(http); 

        setLoginView(http, LoginView.class); 
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }

    @Bean
    public UserDetailsManager userDetailsService() {
        UserDetails user =
                User.withUsername("NormalerAnwender")
                        .password("{noop}user")
                        .roles("STANDARDROLLE")
                        .build();
        UserDetails admin =
                User.withUsername("Administrator")
                        .password("{noop}admin")
                        .roles("ADMINROLLE")
                        .build();
        return new InMemoryUserDetailsManager(user, admin);
    }
}

Unseren beiden hart-kodierten Anwender, NormalerAnwender und Administrator, dienen natürlich nur dem Beispiel. Im echten Leben sollte ein UserDetailsManager natürlich anders aussehen.

Annotationen für die View Klassen

Die folgenden Annotationen schränken den Zugriff auf Ihre Views ein. Wenn keine Annotation auf einen View gesetzt ist, dann greift automatisch @DenyAll, was den Zugriff auf den View komplett und für jeden Anwender verhindert.
@PermitAll erlaubt den Zugriff auf die View durch jeden (authentifizierten) Anwender:

@Route(value = "private", layout = MainView.class)
@PageTitle("Ein privater View")
@PermitAll
public class PrivateView extends VerticalLayout {
    // ...
}

@RolesAllowed erlaubt den Zugriff durch anschliessend definierte Anwederrollen:

@Route(value = "admin", layout = MainView.class)
@PageTitle("Ein View nur für den Admin")
@RolesAllowed("ADMINROLLE")
public class AdminView extends VerticalLayout {
    // ...
}

Man beachte, dass auch die Rollen case-sensitive sind!
@AnonymousAllowed erlaubt es jedem, auch nicht authentifizierten Anwendern, auf eine View zuzugreifen. Diese Annotation sollte man mit Vorsicht und nur für öffentliche Views verwenden:

@Route(value = "", layout = MainView.class)
@PageTitle("Eine öffentliche View")
@AnonymousAllowed
public class PublicView extends VerticalLayout {
    // ...
}

Bei diesen Annotationen sollte einem bewusst sein, dass sie vererbbar sind, also von einer Parent Klasse an die Child Klassen weitergegeben werden. Dies kann jedoch durch annotieren der Child Klasse überschrieben werden. Hierbei gilt: @DenyAll überschreibt alle anderen Annotationen, @AnonymousAllowed überschreibt @RolesAllowed und @PermitAll, und @RolesAllowed überschreibt @PermitAll.

Fazit

Mit diesen Schritten haben wir unseren Anwendern die Möglichkeit gegeben sich ein- und wieder auszuloggen. Auch haben wir die Mittel kennengelernt, mit denen wir die Zugriffsrechte für jede unserer Views gezielt einschränken können.

Wenn Sie daran interessiert sind, Vaadin auf eigene Faust zu lernen, können wir Ihnen dabei helfen, da wir maßgeschneiderte Vaadin Trainings anbieten.