Teil 5: Grundgerüst für die Anwendung

Backend meets Frontend Reloaded: Anmeldeprozess & Co. – die ersten Funktionen

Sven Ruppert

© Shutterstock / HelenField & bkf (modifiziert)

Im letzten Teil der Serie haben wir uns noch einmal ausgiebig mit dem Thema i18n beschäftigt. Der nächste Schritt wird nun sein, das Grundgerüst einer Anwendung zu erstellen. Hier gibt es sicherlich etliche Wege. Da wir nicht die ersten sind, die sich mit dieser Aufgabe konfontiert sehen, gibt es auch einige Ansätze im Open-Source-Bereich.

Die Anforderungen

Nachdem die Anmeldeseite konzipiert worden ist, werden wir nun die Funktionen hinzufügen. Hier gibt es einiges zu beachten. Zum Beispiel sollte man sicherstellen, dass auf jeden Fall ein Anmeldeprozess durchlaufen wird, bevor man die Anwendung verwenden kann. Man sollte zudem verhindern, dass über bekannte URLs in die Anwendung direkt hineingesprungen werden kann. Hierfür gibt es verschiedene Ansätze. Da es sich bei Vaadin Flow um einen serverseitigen Ansatz handelt, kann man sich in der hier gezeigten Implementierung vollständig innerhalb von Java bewegen.

Die Log-in-Seite

Beginnen wir damit, die Log-in-Seite, welche wir bisher nur grafisch erstellt hatten, mit der notwendigen Logik zu befüllen. Zur Erinnerung, wir hatten neben den beiden Textfeldern für die Eingabe des Benutzernamens und des Passwortes schon die beiden Buttons erstellt, mit denen man zum einen den Anmeldeprozess beenden und starten kann.

Der Button zum Abbrechen des Anmeldevorganges hat lediglich die Aufgabe, die beiden Eingabefelder wieder von den beisher getätigten Eingaben zu befreien. Hier reicht ein einfaches aufrufen der Methode clear an beiden Textfeldern aus.

    btnCancel.addClickListener(e -> {
      username.clear();
      password.clear();
    });

Ein wenig umfangreicher wird es, wenn wir uns den Anmeldeprozess selbst ansehen. Dafür müssen verschiedene Dinge passieren. Als erstes benötigen wir die beiden Eingaben der Textfelder, diese müssen auf ihre Gültigkeit hin überprüft werden. Wenn die Prüfung erfolgreich verlaufen ist, kann auf die internen Seiten weitergeleitet werden. Wenn es zu einem Fehlschlag kommt, soll man auf der Anmeldeseite verbleiben.

btnLogin.addClickListener(e -> {
      securityService.checkLogin(username.getValue() ,
                                 password.getValue())
                     .ifPresentOrElse(u -> {
                                        UI.getCurrent()
                                          .getSession()
                                          .setAttribute(SecurityService.User.class , u);
                                        UI.getCurrent()
                                          .navigate(MainView.class);
                                      } ,
                                      f -> {
                                        UI.getCurrent()
                                          .getSession()
                                          .setAttribute(SecurityService.User.class , null);
                                        logger().info(f);
                                      });
    });

Die Implementierung des hier verwendeten SecurityService ist eher als Platzhalter zu verstehen. Es wird hier lediglich die fest verdrahtete Kombination admin admin überprüft.

public class SecurityService {

  public static class User extends Pair<String, LocalDateTime> {
    public User(String name , LocalDateTime timestamp) {
      super(name , timestamp);
    }
  }

  public Result<User> checkLogin(String username , String password) {
    return match(
        matchCase(() -> failure("securityservice.login.denied")) ,
        matchCase(() -> username == null , () -> failure("securityservice.username.null")) ,
        matchCase(username::isEmpty , () -> failure("securityservice.username.is-empty")) ,
        matchCase(() -> password == null , () -> failure("securityservice.password.null")) ,
        matchCase(password::isEmpty , () -> failure("securityservice.password.is-empty")) ,
        matchCase(() -> username.equals("admin") && password.equals("admin") ,
                  () -> success(new User("Jon Doe" , LocalDateTime.now())))
    );
  }
}

Kommen wir nun zu dem Teil, der sich damit beschäftigt, einen Mechanismus zu implementieren, der sich in den globalen Mechanismus von Flow auf der Serverseite einhängt um eine definierte Bedingung zu prüfen. In unserem Fall ist es die Bedingung, ob ein Anmeldeprozess stattgefunden hat, oder besser gesagt, ob sich in der Session ein Attribut vom Typ User befindet. Die Annahme, dass ein Anmeldeprozess stattgefunden hat, ist implizit darauf aufbauend. Das Darf man unter dem Gesichtspunkt der Sicherheit nicht vollständig außer Acht lassen.

ApplicationServiceInitListener

Um sich in den globalen Lebenszyklus einer Vaadin-Flow-Anwendung einzuklinken, gibt es das Interface VaadinServiceInitListener. In diesem Projekt bekommt die Implementierung den Namen ApplicationServiceInitListener. Um einen solchen ServiceInitListener zu aktivieren, muss man den vollständingen Namen der Klasse in der Datei com.vaadin.flow.server.VaadinServiceInitListener angeben. Die Datei ist selbst zu erzeugen und wird in dem Verzeichnis resources/META-INF/services erwartet. Hiermit handelt es sich um einen Standardvorgang zum registrieren von Services innerhalb von Java.

Wer einen Listener registrieren möchte, der bei jeder Initialisierung einer UI aktiviert wird, muss sich von dem ServiceInitEvent ausgehend die Quelle holen und dort einen UIInitListener hinzufügen. Ich muss zugeben, dass dieser Weg nicht intuitiv ist und man es in der Dokumentation erst einmal finden muss, um es verwenden zu können. Allerdings ist dieses Vorgehen in Vaadin Flow an dieser Stelle generisch gelöst, um in den Lebenszyklus eingreifen zu können. Wann immer man auch solche Querschnittsthemen hat, lohnt sich ein Blick in genau diesen Mechanismus.

public class ApplicationServiceInitListener implements VaadinServiceInitListener, HasLogger {

  @Override
  public void serviceInit(ServiceInitEvent e) {

    e.getSource()
     .addUIInitListener((UIInitListener) uiInitEvent -> {
       logger().info("init SecurityListener for .. " + uiInitEvent.getUI());
       uiInitEvent.getUI().addBeforeEnterListener(new SecurityListener());
     });
  }
}

SecurityListener

Zu guter Letzt kommen wir zu der reinen Implementierung des Listeners. Die Klasse ist in diesem Projekt als innere statische Klasse implementiert. Das zu implementierende Interface ist BeforeEnterListener. Dazu gibt es korrespondierend noch weitere Interfaces für die Events, die erzeugt werden, wenn eine Session abgebaut wird.

Was soll nun geschehen? Zunächst wird überprüft, ob sich das gesuchte Attribut in der Session befindet.

vaadinSession.getAttribute(SecurityService.User.class)

Ist dieses vorhanden, geht man davon aus, dass der Anmeldeprozess erfolgreich verlaufen ist. Ist das Attribut nicht in der Session vorhanden, wird zusätzlich noch geprüft, ob das derzeitige Navigationsziel die Anmeldeseite ist. Wenn dem nicht so ist, wird ein Redirect auf die Anmeldeseite initiiert. Macht man den letzten Schritt nicht, wird man übrigens mit einem Stack Overflow konfrontiert.

  private static class SecurityListener implements BeforeEnterListener, HasLogger {
    @Override
    public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
      final UI ui = UI.getCurrent();
      final VaadinSession vaadinSession = ui.getSession();

      Result.ofNullable(vaadinSession
                            .getAttribute(SecurityService.User.class))
            .ifPresentOrElse(u -> {
                               logger().info("User is logged in : " + u);
                             } ,
                             failed -> {
                               logger().info("Anonymous User: redirecting to Login View");
                               if (! beforeEnterEvent.getNavigationTarget().equals(LoginViewOO.class))
                                 beforeEnterEvent.rerouteTo(NAV_LOGIN_VIEW);
                             });

    }
  }

Fazit

Hiermit haben wir nun alles zusammen, um den Lebenszyklus auf der Serverseite zu modifizieren. Durchgeführt haben wir dieses exemplarisch mit einer meistens benötigten Funktion, die nun als Ausgangspunkt für die eigene Entwicklung genommen werden kann.

Als nächstes werden wir uns ansehen, wie man das Grundgerüst der grafischen Oberfläche für eine Anwendung erzeugen kann. Dazu sehen wir uns veschiedene Open-Source-Implementierungen an, die an dieser Stelle einen gewissen Komfort liefern.

Das Beispiel zu diesem Teil ist wie immer auf GitHub zu finden.

Wer Fragen und Anmerkungen hat, meldet sich am besten per Twitter an @SvenRuppert oder direkt per Mail an sven.ruppert@gmail.com

Happy Coding!

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: