REST API-design och DTO: en present utan omslag är riskabel

”Va? Varför syns lösenordet?”

I början på jobbet sprang jag desperat fram och tillbaka mellan frontend och backend. En dag höll jag på att koppla in ‘API:t för medlemslistan’, som en kollega som började samtidigt som jag hade byggt, i frontend-vyn.

Jag öppnade fliken Network i Chrome DevTools (F12) för att kontrollera att datan kom in som den skulle, och i samma ögonblick som jag såg svaret trodde jag inte mina egna ögon.

[
  {
    "id": 1,
    "username": "tester",
    "password": "$2a$10$D...", // Krypterat losenord lackt
    "ssn": "900101-1...",      // Personnummer lackt
    "createdAt": "2024-01-01"
  }
]

Chockad frågade jag kollegan: ”Du, varför syns lösenordet och till och med personnumret i det här API-svaret?”

Han svarade helt lugnt: ”Jaha, det? På skärmen visar vi ju ändå bara namnet. Jag orkade inte hålla på, så jag returnerade bara resultatet av userRepository.findAll() rakt av. Är det ett problem?”

Det gick en kall kår längs ryggraden. Han sökte bara bekvämlighet, men hade i praktiken planterat fröet till en allvarlig säkerhetsincident.

”Bara för att det inte syns på skärmen betyder det inte att det är säkert!”

Vem som helst kan öppna webbläsarens nätverksflik. Vad händer om en illvillig angripare anropar detta API?

Att skicka ut en entity rakt av var inte olikt att göra sovrummet hemma till en helt genomskinlig glasvägg.

En genomskinlig påse där allt innehåll syns är i praktiken en inbjudan för tjuvar.

DTO: datans ‘presentpapper’

En ‘Entity’ är ett slags ‘råmaterial’ som bara hanteras inne i fabriken. Där finns sådant som kunder aldrig ska se, som hemligheter (lösenord) eller intern administrationsdata (skapad-datum, uppdaterad-datum, borttagningsflagga).

En ‘DTO(Data Transfer Object)’ är däremot den ‘färdiga produkten’, snyggt paketerad för leverans till kunden.

Att returnera en entity direkt är som att kasta en blodig bit rått kött på tallriken i stället för att servera en tillagad biff. Vi måste absolut ha ett steg av ‘mappning’ emellan.

[Code Verification] Den oändliga loopens träsk (cirkulär referens)

Det som driver utvecklare ännu mer till vansinne än säkerhetsproblem är problemet med ‘cirkulära referenser’. Kommer du ihåg den dubbelriktade JPA-mappningen vi gick igenom senast, User <-> Team?

Vad händer om man returnerar User-entiteten direkt efter att ha omvandlat den till JSON?

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

    @ManyToOne // En anvandare tillhor ett lag
    private Team team;
}

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

    @OneToMany(mappedBy = "team") // Ett lag har flera anvandare
    private List<User> users = new ArrayList<>();
}

Om du returnerar User i det här läget fungerar JSON-serialiseraren (Jackson) ungefär så här.

Resultat: servern dör en heroisk död med ett StackOverflowError. Relationerna mellan entities fortsätter att jaga varandra i all oändlighet, så om du inte bryter kedjan och exponerar allt rakt av hamnar du i en oändlig loop. Det är den avgörande anledningen till att använda ett DTO och bara ta med de fält som faktiskt behövs, som teamName.

Om du inte bryter relationskedjan med ett DTO fortsätter datan att snurra för alltid.

Villkoret för ett bra API: att vara RESTful

När du har paketerat datan ordentligt med DTO:er återstår det att bestämma rätt ‘adress’ dit paketet ska skickas. Det är precis vad REST API-design handlar om.

På universitetet brukade jag döpa URL:er helt som jag själv ville.

Det är ingen bra idé att stoppa in verb som join eller update i URL:en. I en RESTful design ligger ‘resursen’ (substantivet) i URL:en, medan ‘handlingen’ (verbet) hör hemma i HTTP-metoden.

När man bygger så här räcker det att se URL:en för att förstå: ”Aha, här hanterar vi användarresurser.” Det blir också mycket enklare att kommunicera med frontend-utvecklare. Det här är en gemensam överenskommelse bland utvecklare över hela världen.

Praktiskt råd: namnet gör halva jobbet (namngivningsstrategi för DTO)

När jag öppnar koden i vårt nuvarande projekt suckar jag ibland direkt. UserDto, MemberVo, InfoParam, ResultData… tidigare utvecklare gav saker namn hur som helst, så det går inte att se om ett objekt är till för request eller response innan man gräver i koden.

Ett DTO är ett ‘kärl’ för data. Om kärlets syfte inte syns i namnet uppstår precis den sorts förvirring där man lägger ris i soppskålen och vatten i risskålen. Den namngivningsregel jag rekommenderar mest i praktiken är denna.

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

    // Svar med anvandarinfo
    public static class Response {
        private String name;
        private String email;
    }
}

När serie 2 avslutas: nu står huset på plats

Därmed är [Monolith Builder]-serien slut. Vi lade grunden med Spring Boot (DI), separerade frontend och backend (CORS), lärde oss hantera databaser med JPA och även hur man kommunicerar via DTO:er och REST API:er.

Nu har vi byggt upp förmågan att på egen hand skapa en ordentlig webbtjänst. Men en utvecklares resa slutar inte här. Vad är poängen om tjänsten jag byggt bara fungerar på min egen dator, alltså localhost? För att visa den för riktiga användare måste den upp på en server.

Men den servern är inte Windows, utan Linux: en svart skärm och inget mer. Utan ens en mus, hur installerar och kör man program där?

I nästa serie, [Works on My Machine], ska vi ge oss in i Linux- och Docker-världen för att lösa utvecklarnas eviga problem: ”Det fungerar på min dator, men inte på servern.”

Lämna en kommentar