Når backend og database samarbejder om validering

I QuestGame arbejder vi med geografiske positioner som en central del af spillet. Spilleren bevæger sig fysisk rundt og skal kunne finde og aktivere forskellige QuestItems, der er placeret på bestemte lokationer. Til lagring af disse positioner bruger vi PostgreSQL med PostGIS, mens selve mobilapplikationen består af React Native og TypeScript kombineret med Unity som spilmotor.

Da jeg har ansvaret for databasen pga. mit specialiseringsområde database og datalagring, har jeg selvfølgelig også spurgt mig selv om det er nødvendigt at bruge PostGIS. Hvis databasen primært skal gemme koordinater, kunne man spørge, om en almindelig latitude- og longitude-værdi ikke ville være tilstrækkelig.

Men hvis man begynder at se på ansvarsfordelingen mellem klient, backend og database, bliver PostGIS mere relevant. Det handler nem lig ikke kun om, hvor data bliver gemt, men også om hvor systemets regler bliver håndhævet.

Hvem bestemmer, om spilleren er tæt nok på?

Forestil dig, at en spiller bevæger sig hen mod et QuestItem. Mobiltelefonens GPS giver løbende spillerens position, og klienten kan bruge denne information til at beregne afstanden til et QuestItem.

Det giver god mening, at denne del foregår lokalt på telefonen. Spilleren skal gerne kunne få løbende feedback uden hele tiden at vente på svar fra serveren.

Brugergrænsefladen kunne eksempelvis vise:

Aktivitet
63 meter væk

Kom tættere på for at starte aktiviteten.

Når spilleren går tættere på, opdateres afstanden løbende.

Problemet opstår, hvis klienten samtidig får lov til selv at bestemme, hvornår QuestItem må aktiveres.

Hvis klienten eksempelvis sender følgende til backend:

{
    "questItemId": 42,
    "distance": 5,
    "canActivate": true
}Code language: JSON / JSON with Comments (json)

stoler backend i praksis på oplysninger, som kommer fra klienten. En modificeret klient ville derfor potentielt kunne fortælle serveren, at spilleren befinder sig tæt på et QuestItem, selv om det ikke er tilfældet.

Det illustrerer et vigtigt princip inden for backendudvikling: klienten bør ikke være autoriteten for centrale forretningsregler.

Backend som validerende lag

En mere robust løsning er, at klienten kun sender de oplysninger, backend behøver for selv at træffe afgørelsen.

Eksempelvis:

{
    "questItemId": 42,
    "latitude": 55.1234,
    "longitude": 10.1234
}Code language: JSON / JSON with Comments (json)

Backend kan derefter finde QuestItemets gemte position og kontrollere, om spilleren befinder sig inden for den tilladte aktiveringsradius.

Flowet bliver dermed:

Spilleren forsøger at åbne et QuestItem
        ↓
Unity / React Native sender spillerens position
        ↓
ASP.NET Core modtager requestet
        ↓
QuestItemets position hentes
        ↓
PostGIS kontrollerer afstanden
        ↓
Backend afgør om QuestItemet må aktiveres

Hvis reglen eksempelvis siger, at spilleren skal være inden for 25 meter, kan PostGIS bruges til at kontrollere det geografiske forhold.

En funktion som ST_DWithin kan afgøre, om to geografiske positioner befinder sig inden for en bestemt afstand.

Konceptuelt kan det beskrives som:

ST_DWithin(
    QuestItemLocation,
    PlayerLocation,
    25
)

Resultatet bruges derefter af backendens forretningslogik.

Det interessante er, at ansvaret her bliver fordelt mellem flere lag. Databasen udfører den geografiske beregning, mens backend bestemmer, hvad resultatet betyder for spillets regler.

PostGIS bestemmer altså ikke, om spilleren “må spille”. Databasen kan eksempelvis svare, at spilleren befinder sig inden for 25 meter. Det er backend, der bruger denne information til at afgøre, om QuestItemet skal kunne aktiveres.

Database og backend arbejder sammen

Denne problemstilling viser meget godt sammenhængen mellem mine to specialiseringsområder: backendudvikling samt databaser og datalagring.

Databasen er ikke nødvendigvis kun et passivt sted, hvor information opbevares. Når man bruger PostgreSQL sammen med PostGIS, får databasen funktioner, der er specialiserede i geografiske beregninger.

Backendens opgave bliver derefter at anvende disse muligheder som en del af systemets forretningslogik.

Man kunne godt vælge at hente begge positioner fra databasen og derefter beregne afstanden direkte i C#. Det ville teknisk set fungere. Fordelen ved PostGIS er imidlertid, at geografiske data behandles som egentlige geografiske datatyper, og at databasen er udviklet til at udføre geografiske forespørgsler effektivt.

Det bliver også interessant, hvis QuestGame f.eks. skal kunne stille spørgsmål som:

Find alle QuestItems inden for 500 meter fra spilleren.

Her kan databasen udføre den geografiske filtrering, før data sendes videre til backend.

Det reducerer både mængden af data, der skal overføres, og mængden af beregninger, backend selv skal udføre.

Feedback når spilleren er for langt væk

Valideringen skal selvfølgelig også kunne mærkes i brugeroplevelsen.

Hvis backend afviser aktiveringen, bør klienten ikke blot ignorere spillerens tryk. Backend kan eksempelvis returnere:

{
    "canActivate": false,
    "distance": 38,
    "activationRadius": 25
}Code language: JSON / JSON with Comments (json)

Klienten kan derefter vise en besked som:

Du er ikke tæt nok på aktiviteten endnu.
Du er cirka 38 meter væk.

Her opstår der igen en opdeling af ansvaret.

Unity eller React Native står for præsentationen og den løbende brugeroplevelse. Backend står for afgørelsen, mens databasen bidrager med den geografiske beregning.

Klienten kan stadig beregne afstand lokalt for at give hurtig feedback, men den endelige validering sker på serveren.

Use case, der beskriver forretningslogikken

Use cases beskriver altid systemet fra brugerens perspektiv, altså som en lukket boks (“black box”). For at kunne beskrive forretningslogikken, arbejder jeg med denne use case – her skrevet i brief format:

Spilleren kommer tæt på en QuestEnhed, og denne bliver aktiv. Når spilleren åbner en QuestEnhed, kontrolleres afstanden af systemet. Spilleren kan dernæst åbne QuestEnheden og udføre den.

Alternativt flow: Hvis spilleren er for langt fra QuestEnheden, får denne en besked om at QuestEnheden er for langt væk og at man skal bevæge sig tættere på.

Det løser ikke GPS-spoofing

Selvom jeg nu har beskrevet en forretningslogik, der kan validere en lokation, er den ikke sikret mod GPS-spoofing.

Selv om backend selv beregner afstanden, kommer spillerens aktuelle koordinater stadig fra telefonen. En bruger, der manipulerer telefonens GPS-data, kan derfor potentielt sende en falsk position.

Backend-valideringen beskytter altså mod, at klienten selv kan ændre regler som aktiveringsradius eller blot sende canActivate = true. Den beskytter ikke nødvendigvis mod en falsk GPS-position.

Et mere avanceret anti-cheat-system kunne blandt andet analysere unaturlige positionsspring, usandsynlige hastigheder eller andre mønstre. Det vil dog være et væsentligt større område og ligger uden for vores nuværende projekt.

MVP vs. produktionssystem

I QuestGame overvejer vi derfor, om denne validering skal implementeres allerede i vores MVP.

En enklere løsning er at lade klienten stå for afstand og aktivering i prototypen. Det gør det muligt at fokusere på selve spiloplevelsen og de vigtigste use cases.

Samtidig kan server-side validering beskrives som en nødvendig videreudvikling, hvis systemet skal anvendes i produktion.

For mig viser eksemplet især, at backend og database ikke bør betragtes som to isolerede områder. Backendens forretningslogik afhænger ofte af de muligheder, databasen stiller til rådighed, mens databasens funktionalitet først får reel betydning, når den bruges til at understøtte systemets regler og use cases.

Kilder