REST-API-Design und DTOs: Ein Geschenk ohne Verpackung ist riskant

„Hä? Warum kann ich das Passwort sehen?“

In meinen ersten Monaten im Unternehmen entwickelte ich hektisch zwischen Frontend und Backend hin und her. Eines Tages band ich die von einem Kollegen aus meinem Einstiegsjahr entwickelte „API zur Mitgliederliste“ an den Frontend-Screen an.

Ich öffnete den Netzwerk-Tab der Chrome-Entwicklertools (F12), um zu prüfen, ob die Daten korrekt ankamen, und als ich die Response-Daten sah, traute ich meinen Augen nicht.

[
  {
    "id": 1,
    "username": "tester",
    "password": "$2a$10$D...", // Verschluesseltes Passwort wird offengelegt
    "ssn": "900101-1...",      // Personalausweisnummer offengelegt
    "createdAt": "2024-01-01"
  }
]

Völlig erschrocken fragte ich ihn: „Hey, warum sieht man in der API-Antwort das Passwort und sogar die Personalausweisnummer?“

Er antwortete ganz locker: „Ach das? Auf dem Screen wird doch sowieso nur der Name angezeigt. Ich war zu faul und habe einfach das Ergebnis von userRepository.findAll() direkt zurückgegeben. Ist das ein Problem?“

Mir lief ein Schauer über den Rücken. Er wollte eigentlich nur bequem sein, hatte damit aber faktisch den Samen für einen ernsten Sicherheitsvorfall gelegt.

„Nur weil es auf dem Bildschirm nicht sichtbar ist, ist es noch lange nicht sicher!“

Jeder kann den Netzwerk-Tab im Browser sehen. Was wäre, wenn ein böswilliger Angreifer diese API aufruft?

Ein Entity unverändert nach außen zu geben, war praktisch so, als würden wir unser Schlafzimmer mit einer gläsernen Wand ausstatten.

Ein transparenter Beutel, dessen Inhalt komplett sichtbar ist, ist für Diebe praktisch eine Einladung.

DTO: das ‘Verpackungspapier’ der Daten

Ein ‘Entity’ ist ein ‘Rohmaterial’, das nur innerhalb der Fabrik verarbeitet wird. Darauf kleben Dinge, die ein Kunde niemals sehen darf, etwa Geheimnisse wie Passwörter oder interne Verwaltungsinformationen wie Erstellungsdatum, Änderungsdatum oder Löschstatus.

Ein ‘DTO(Data Transfer Object)’ hingegen ist ein sauber verpacktes ‘Fertigprodukt’, das an den Kunden ausgeliefert werden kann.

Ein Entity direkt zurückzugeben ist so, als würde man dem Gast im Restaurant kein fertiges Steak servieren, sondern ihm blutiges rohes Fleisch auf den Teller werfen. Wir brauchen zwingend einen Schritt der ‘Transformation (Mapping)’.

[Code Verification] Der Sumpf der Endlosschleife (zirkuläre Referenz)

Was Entwickler noch mehr in den Wahnsinn treibt als Sicherheitsprobleme, ist das Problem der ‘zirkulären Referenz (Circular Reference)’. Erinnern Sie sich an das bidirektionale JPA-Mapping aus der letzten Folge, User <-> Team?

Was passiert wohl, wenn man das User-Entity direkt in JSON umwandelt und zurückgibt?

@Entity
public class User {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @ManyToOne // Ein Nutzer gehoert zu einem Team
    private Team team;
}

@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team") // Ein Team hat mehrere Nutzer
    private List<User> users = new ArrayList<>();
}

Wenn man in diesem Zustand User zurückgibt, arbeitet der JSON-Serializer (Jackson) ungefähr so.

Ergebnis: Der Server stirbt heldenhaft mit einem StackOverflowError. Beziehungen zwischen Entities hängen aneinander wie Glieder einer Kette. Wenn man sie nicht sauber kappt und einfach so hinausgibt, landet man in einer Endlosschleife. Genau deshalb braucht man ein DTO, das nur die wirklich benötigten Felder wie teamName enthält.

Wenn man die Beziehungskette nicht mit einem DTO kappt, rotiert der Datenstrom endlos weiter.

Was eine gute API ausmacht: RESTful

Wenn die Daten mit DTOs sauber verpackt sind, muss man jetzt nur noch die richtige ‘Adresse’ für dieses Paket festlegen. Genau das ist REST-API-Design.

Zu Studienzeiten habe ich URLs einfach nach Lust und Laune benannt.

Verben wie join oder update gehören nicht in die URL. Bei einem RESTful-Design stehen ‘Ressourcen’ (Substantive) in der URL, und die ‘Aktion’ (Verb) wird dem HTTP-Method überlassen.

Wenn man es so strukturiert, erkennt man schon an der URL: „Aha, hier geht es um die Ressource User.“ Außerdem wird die Kommunikation mit Frontend-Entwicklern viel einfacher. Das ist das gemeinsame Versprechen von Entwicklern auf der ganzen Welt.

Praxis-Tipp: Der Name ist die halbe Miete (DTO-Namensstrategie)

Wenn ich heute den Code unseres Firmenprojekts öffne, seufze ich manchmal schon beim ersten Blick. UserDto, MemberVo, InfoParam, ResultData… Die Vorgänger haben Dinge nach Belieben benannt, sodass man vor dem Blick in den Code gar nicht weiß, ob es sich um ein Request- oder ein Response-Objekt handelt.

Ein DTO ist ein ‘Behälter’ für Daten. Wenn der Verwendungszweck dieses Behälters nicht schon im Namen steht, entsteht genau die Art von Chaos, bei der Suppe in die Reisschüssel und Wasser in die Suppenschüssel kommt. Die Namensregel, die ich in der Praxis am meisten empfehle, ist die folgende.

public class UserDto {
    // Registrierungsanfrage
    public static class SignUpRequest {
        private String email;
        private String password;
    }

    // Nutzerinfo-Antwort
    public static class Response {
        private String name;
        private String email;
    }
}

Zum Ende von Serie 2: Das Haus steht jetzt

Damit endet die Serie [Monolith Builder]. Wir haben mit Spring Boot (DI) das Fundament gelegt, Frontend und Backend getrennt (CORS), mit JPA die Datenbank angebunden und schließlich gelernt, über DTOs und REST-APIs sauber zu kommunizieren.

Jetzt besitzen wir die Fähigkeit, auch allein einen ordentlichen Webservice zu bauen. Aber die Reise eines Entwicklers endet nicht hier. Was bringt es, wenn mein Service nur auf meinem eigenen Rechner, also auf Localhost, funktioniert? Um ihn echten Nutzern zu zeigen, muss er auf einen Server.

Nur ist dieser Server kein Windows-Rechner, sondern Linux, eine schwarze Kommandozeile und sonst nichts. Wie installiert und startet man dort Programme, wenn es nicht einmal eine Maus gibt?

In der nächsten Serie [Works on My Machine] tauchen wir in die Welt von Linux und Docker ein, um das ewige Entwicklerproblem zu lösen: „Auf meinem Rechner funktioniert es, aber auf dem Server nicht.“

Schreibe einen Kommentar