REST API-ontwerp en DTO: een cadeau zonder verpakking is riskant

“Hè? Waarom kan ik het wachtwoord zien?”

In mijn beginperiode bij het bedrijf was ik koortsachtig bezig tussen frontend en backend heen en weer te springen. Op een dag koppelde ik de ‘API voor het ophalen van de ledenlijst’, gebouwd door een collega die tegelijk met mij was begonnen, aan het frontend-scherm.

Ik opende het Network-tabblad in Chrome DevTools (F12) om te controleren of de data goed binnenkwam, en op het moment dat ik de response zag, geloofde ik mijn ogen niet.

[
  {
    "id": 1,
    "username": "tester",
    "password": "$2a$10$D...", // Versleuteld wachtwoord blootgesteld
    "ssn": "900101-1...",      // BSN (burgerservicenummer) blootgesteld
    "createdAt": "2024-01-01"
  }
]

Geschrokken vroeg ik mijn collega: “Hé, waarom zijn in deze API-response het wachtwoord en zelfs het burgerservicenummer zichtbaar?”

Hij antwoordde heel relaxed: “Oh, dat? Op het scherm tonen we toch alleen de naam. Ik had geen zin in extra werk, dus ik heb gewoon het resultaat van userRepository.findAll() direct teruggegeven. Is dat een probleem?”

Er liep een koude rilling over mijn rug. Hij zocht alleen gemak, maar had in de praktijk het zaadje geplant voor een ernstig beveiligingsincident.

“Dat het niet op het scherm staat, betekent nog niet dat het veilig is!”

Iedereen kan het netwerk-tabblad van de browser openen. Wat als een kwaadwillende aanvaller deze API zou aanroepen?

Een entity onbewerkt naar buiten sturen was hetzelfde als van de hoofdslaapkamer in ons huis een volledig glazen wand maken.

Een transparante zak waarvan de inhoud volledig zichtbaar is, is voor een dief praktisch een uitnodiging.

DTO: het ‘inpakpapier’ van data

Een ‘Entity’ is een soort ‘grondstof’ die alleen binnen de fabriek wordt verwerkt. Er kleven dingen aan die je nooit aan een klant mag tonen, zoals geheimen (wachtwoorden) of interne beheerinformatie (aanmaakdatum, wijzigingsdatum, verwijdervlag).

Een ‘DTO(Data Transfer Object)’ is daarentegen het ‘eindproduct’, netjes verpakt om aan de klant te worden geleverd.

Een entity direct teruggeven is alsof je in een restaurant geen gebakken steak serveert, maar een bloederig stuk rauw vlees op het bord gooit. We hebben absoluut een ‘mapping’-stap nodig.

[Code Verification] Het moeras van de oneindige lus (circulaire referentie)

Wat developers nog gekker maakt dan beveiligingsproblemen is het probleem van de ‘circulaire referentie’. Weet je het bidirectionele JPA-mapping van de vorige keer nog, User <-> Team?

Wat gebeurt er als je de User-entity direct naar JSON omzet en terugstuurt?

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

    @ManyToOne // Een user hoort bij een team
    private Team team;
}

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

    @OneToMany(mappedBy = "team") // Een team heeft meerdere users
    private List<User> users = new ArrayList<>();
}

Als je User in deze toestand terugstuurt, werkt de JSON-serializer (Jackson) ongeveer zo.

Resultaat: de server sterft een heroïsche dood met een StackOverflowError. Relaties tussen entities blijven elkaar achtervolgen, dus als je die keten niet doorknipt en alles zomaar blootstelt, beland je in een oneindige lus. Dat is precies de doorslaggevende reden om een DTO te gebruiken en alleen de velden op te nemen die je echt nodig hebt, zoals teamName.

Als je de relatieketen niet met een DTO doorknipt, blijft de data eeuwig ronddraaien.

Voorwaarde voor een goede API: RESTful

Als je de data netjes hebt verpakt met DTO’s, moet je nu het juiste ‘adres’ bepalen waarheen dat pakket wordt verzonden. Dat is precies waar REST API-ontwerp over gaat.

Op de universiteit gaf ik URL’s gewoon namen waar ik zelf zin in had.

Werkwoorden als join of update horen niet thuis in de URL. In een RESTful ontwerp komt de ‘resource’ (zelfstandig naamwoord) in de URL en laat je de ‘actie’ (werkwoord) over aan de HTTP-method.

Als je het zo opbouwt, zie je al aan de URL: “Aha, dit gaat over user-resources.” Ook de communicatie met frontend developers wordt veel makkelijker. Dit is de gedeelde afspraak van developers over de hele wereld.

Praktisch advies: de naam doet al de helft (DTO-naamgevingsstrategie)

Soms open ik de code van ons huidige bedrijfsproject en zucht ik meteen. UserDto, MemberVo, InfoParam, ResultData… vorige developers hebben alles genoemd zoals het hun uitkwam, waardoor je pas na het lezen van de code weet of een object bedoeld is voor requests of responses.

Een DTO is een ‘bakje’ voor data. Als het doel van dat bakje niet in de naam staat, krijg je precies de verwarring waarbij je soep in een rijstkom schept en water in een soepkom giet. De naamregel die ik in de praktijk het meest aanbeveel is deze.

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

    // Gebruikersinfo antwoord
    public static class Response {
        private String name;
        private String email;
    }
}

Aan het einde van serie 2: het huis staat nu

Daarmee eindigt de serie [Monolith Builder]. We hebben de fundering gelegd met Spring Boot (DI), frontend en backend gescheiden (CORS), geleerd hoe we met JPA databases aanpakken en zelfs hoe we communiceren via DTO’s en REST API’s.

Nu hebben we het vermogen opgebouwd om in ons eentje een degelijke webservice te bouwen. Maar de reis van een developer eindigt hier niet. Wat heb je eraan als de service die ik heb gebouwd alleen werkt op mijn eigen computer, dus op localhost? Om die aan echte gebruikers te tonen, moet hij naar een server.

Maar die server is geen Windows-machine, maar Linux: een zwart scherm en verder niets. Hoe installeer en start je daar programma’s, zonder zelfs maar een muis?

Laten we in de volgende serie, [Works on My Machine], de wereld van Linux en Docker induiken om het eeuwige developerprobleem op te lossen: “Op mijn machine werkt het, maar op de server niet.”

Plaats een reactie