Spring Security – JSON Web-Token-Authentifizierung (1/3)

Die Sicherheit von Web-Applikationen (Web-Apps) ist eine wichtige Aufgabe. Bei statischen Webseiten, deren Inhalt man nur lesen kann, wie z. B. bei Blogs oder Informationseiten, ist die Sache ziemlich einfach.

Das Problem beginnt, wenn wir dem Nutzer die Möglichkeit eröffnen, Daten und Dateien mittels eines Formulars an den Server zu schicken oder wenn wir Benutzer in unserer App registrieren. Hier öffnen wir das System eventuell für einen schädlichen Code oder riskieren, dass Unberechtigte Zugriff auf Dateien bekommen, die sie eigentlich nicht sehen sollten.

Wenn alle Nutzer einen unbeschränkten Zugriff auf alle Inhalte hätten, wäre ein heilloses Chaos noch das harmloseste Szenario.

Das Spring Framework bietet hier eine gute Lösung: Es enthält ein ganzes Kontingent an Hilfsmitteln, die uns den Schutz unserer App erleichtern. Die Bibliothek liefert uns zum Beispiel mehrere fertige Bestandteile, wie Filter, die einkommende Requests überprüfen oder ein Gerüst für eine User-Rollen-Verwaltung. Wir können definieren, welche Ressourcen in unserer App öffentlich erreichbar sind und welche eine Authentifizierung fordern.

Spring Security bietet damit reichlich Optionen auf diesem Gebiet.  Das möchte ich im Folgenden am Beispiel einer Spring Web-App zeigen, die am Backend eine JSON Web Token Authentication implementiert. In der Beispiel-App kann man über ein öffentlich zugängliches Formular einen Versicherungsantrag einreichen. Es gibt die Möglichkeit, sich auf der Webseite zu registrieren. Wenn man ein Konto angelegt hat, kann man alle Anträge sehen und sie akzeptieren oder ablehnen.

Diese Technologien und Tools kamen dabei zum Einsatz:

  • Spring
  • Maven
  • Spring Security
  • Postman
  • Hibernate
  • MySQL
  • Lombok
  • Frontend: Angular

Welche Arten der Authentifizierung gibt es?

Wenn sich ein User in einer Web-App anmeldet, läuft im Hintergrund eine Authentifizierungsroutine ab. Hier gibt es unterschiedliche Methoden, wie z. B.

Sessions

Nach der erfolgreichen Authentifizierung wird eine Session auf dem Server erstellt. Benutzer- und Session-Details werden dort gespeichert. Der Administrator hat dadurch zwar mehr Kontrolle über Sessions und angemeldete Benutzer, aber diese Herangehensweise verlangt auch relativ viel Aufwand  vom Server – die Sessions müssen irgendwo, also entweder im Arbeitsspeicher oder in einer Datenbank aufbewahrt werden. Ein weiteres Problem entsteht, wenn wir unsere Applikation skalieren wollen und weitere Server hinzufügen. Die Sessions müssen dann synchronisiert werden, was die Komplexität der Implementierung erhöht und zu Bugs führen kann.

JSON Web Tokens (JWTs)

Der Server legt in diesem Fall keine Sessions an. Seine Rolle ist darauf beschränkt, Requests zu überprüfen und Tokens zu erstellen. Es ist dann die Aufgabe des Clients, diese zu behalten. Die Anmeldung erfolgt beispielsweise über ein HTML-Formular, indem der User wie gewohnt seinen Benutzernamen, seine E-Mail-Adresse und sein Passwort eingibt. Das Backend prüft, ob die eingegebenen Daten mit den gespeicherten in der Datenbank übereinstimmen. Sollte das der Fall sein, wird ein JWT generiert und als Response an das Frontend versendet. Von der Client-Seite wird das Token übernommen und mit den nächsten Requests in die Headers hinzugefügt. Das Token enthält ein verschlüsseltes Claim, das uns den Zugriff auf bestimmte Resourcen berechtigt. Falls dieses Claim von jemandem modifiziert wird, wird das im Backend erkannt, und der Request wird abgelehnt. Das hat gleich zwei Vorteile: Erstens belastet diese Methode den Server geringer, weil die Session-Informationen auf der Client-Seite aufbewahrt werden. Zweitens eignet sie sich gut für Microservices-Architekturen.

OAuth

Der Terminus OAuth kommt im Umfeld von Security-Themen sehr oft vor, aber was bedeutet er eigentlich? Laut Definition ist OAuth ein offenes Authorisierungsprotokoll, das beschreibt, wie unabhängige Services Zugang zu ihren Ressourcen erlauben können, ohne initiale Credentials miteinander zu teilen. Die Authentifizierung erfolgt durch einen zwischengeschalteten Server, der die Anmeldedaten des Users überprüft. Das kann der Server eines Anbieters wie Github, Facebook oder LinkedIn sein. Nach Eingabe von Login und Passwort erhalten wir ein Token, das unsere Identität bestätigt. Dank dieser Methode braucht sich der Anwender nicht in unserer Web-App zu registrieren. Die Authentifizierung erfolgt beim externen Provider. Vorausetzung dafür ist, dass der Anwender bereits ein Konto bei diesem hat. Auch JWTs können diese Methode benutzen.

Beispiel einer Oauth-Anmeldung bei exercism.io. (Hier meldet sich der User über ein Konto bei Github an)

Aufbau und Funktionalität

Spring Security „baut“ eine zusätzliche Schicht für unsere Applikation. Einkommende Requests gehen durch diese Schicht hindurch. Wie die Requests behandelt werden, wird in der Konfigurationsklasse definiert. Im JWT-Workflow wird ein Request zuerst gefiltert. Dabei wird überprüft, ob das Token korrekt ist. Sollte das nicht der Fall sein, wird der Request abgelehnt. Andernfalls wird er zu den Endpoints weitergeleitet.

Die Anmeldung

Für die Anmeldung schicken wir einen Request zu einem Auth Endpoint – der natürlich freigegeben sein sollte. Der AuthenticationManager hält eine Referenz zum UserDetailsService. Dieser gleicht die Credentials mit unserer Datenbank ab. Wenn die Daten übereinstimmen, wird ein Authentication Object gebaut, das als Basis für die Generierung eines JWT‘s dient. Wenn der Vorgang beendet ist, sendet der Server die Antwort mit dem Token zurück.

Das Frontend muss ihn übernehmen und lokal abspeichern. Bei den nächsten Requests wird er in den Headers hinzugefügt. Auf diese Weise können wir die gewünschten Ressourcen erreichen.

Dieses Bild hat ein leeres Alt-Attribut. Der Dateiname ist image-2.png
Projektstruktur aus unserem Beispiel. Pakete die nicht relevant für die Security sind, werden versteckt.
  1. Benutzerregistrierung

Fangen wir mit der Benutzerregistrierung an:

Wenn wir ein Konto zu erstellen möchten, senden wir einen RegisterRequest an einen öffentlichen Endpoint. Die Klasse benutzt einen von Spring gelieferten PasswordEncoder und ein Autowired ApplicationUserRepository. Beim RegisterRequest haben wir eine einfache Validierung implementiert, damit Benutzername oder Password nicht null oder leer sind. Nach der Validierung wird der Benutzer in der Datenbank gespeichert.

Im Projekt nutzen wir die Java Bibliothek Lombok – deswegen müssen wir Getters und Setters nicht manuell schreiben. Die @Data Annotation generiert uns auch einen Constructor mit allen notwendigen Feldern inklusive hashCode() und equals()-Methoden.

RegisterController:

@RestController
@RequestMapping("/v1/claimapp/users/")
public class RegisterController {
    private ApplicationUserRepository applicationUserRepository;
    private PasswordEncoder passwordEncoder;

    public RegisterController(ApplicationUserRepository applicationUserRepository,
                              PasswordEncoder passwordEncoder) {
        this.applicationUserRepository = applicationUserRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @RequestMapping(method = RequestMethod.POST, value = "register")
    public void signUp(@RequestBody @Valid RegisterRequest registerRequest) {
        ApplicationUser user = new ApplicationUser();
        user.setUsername(registerRequest.getUsername());
        user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
        applicationUserRepository.save(user);
    }
}

RegisterRequest:

@Data
public class RegisterRequest {

    @NotNull
    @NotBlank
    @Size(min = 4)
    private String username;

    @NotNull
    @NotBlank
    @Size(min = 4)
    private String password;
}

ApplicationUser:

@Entity
@Data
public class ApplicationUser {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @NotNull
    @NotBlank
    @Size(min = 4)
    private String username;

    @NotNull
    @NotBlank
    @Size(min = 4)
    private String password;
}

ApplicationUserRepository:

public interface ApplicationUserRepository extends JpaRepository<ApplicationUser, Long> {
    Optional<ApplicationUser> findByUsername(String username);
}

Spring Security – JSON Web-Token-Authentifizierung (2/3) | Spring Security – JSON Web-Token-Authentifizierung (3/3)

Kommentar verfassen

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.

%d Bloggern gefällt das: