Cookies, sessions & CORS
HTTP is stateless: de server onthoudt tussen twee requests niets over jou. Toch blijf je ingelogd terwijl je van pagina naar pagina klikt. Deze pagina legt uit hoe dat kan, en waarom precies datzelfde mechanisme een beveiligingsregel nodig maakt die je bij je eerste fetch() naar een andere server tegen de muur laat lopen.
Stateless: elk request begint van nul
Stel je twee requests voor, een halve seconde na elkaar:
GET /profiel HTTP/1.1 ← wie ben jij?
GET /bestellingen HTTP/1.1 ← en wie ben jij?
De server heeft geen geheugen van het eerste wanneer het tweede binnenkomt. Dat is bewust: het maakt servers eenvoudig en schaalbaar. Een verzoek kan door de ene machine behandeld worden en het volgende door een andere, zonder dat iemand iets moet doorgeven.
Het gevolg: wie je bent, moet bij elk request opnieuw blijken. Daarvoor bestaan twee gebruikelijke oplossingen: een cookie, of een token in de Authorization-header.
Cookies
Een cookie is een klein stukje tekst dat de server aan de browser geeft en dat de browser daarna automatisch terugstuurt bij elk volgend request naar diezelfde site.
1. Je logt in
POST /login -> gebruiker + wachtwoord
2. De server antwoordt en geeft een cookie mee
HTTP/1.1 200 OK
Set-Cookie: session=a3f9c1; Path=/; HttpOnly; Secure; SameSite=Lax
3. Elk volgend request van je browser draagt het cookie
GET /profiel HTTP/1.1
Cookie: session=a3f9c1
De browser doet stap 3 helemaal zelf. Jij hoeft daar in je code niets voor te doen, en precies dat automatisme is later de kern van het CORS-verhaal.
De attributen die ertoe doen
| Attribuut | Wat het doet | Waarom het belangrijk is |
|---|---|---|
HttpOnly | JavaScript kan het cookie niet lezen | Beperkt de schade als er kwaadaardige code op je pagina belandt |
Secure | Wordt enkel over HTTPS verstuurd | Anders staat je sessie leesbaar op de lijn |
SameSite=Lax / Strict | Beperkt meesturen bij verzoeken vanaf andere sites | Beschermt tegen verzoeken die een andere site in jouw naam doet |
Max-Age / Expires | Hoelang het cookie geldig blijft | Zonder deze verdwijnt het bij het sluiten van de browser |
Path / Domain | Voor welke paden en domeinen het meegaat | Beperkt de verspreiding van je sessie |
DevTools → Application → Cookies. Je ziet per site de naam, de waarde en de attributen. Zoek er een sessiecookie met HttpOnly aangevinkt: die kan je met JavaScript niet uitlezen, ook niet in je eigen console.
Sessions
Een cookie hoeft je gegevens niet te bevatten, en dat is meestal ook niet de bedoeling. Bij een session bevat het cookie enkel een onvoorspelbaar sessie-id; alles wat daarbij hoort, blijft op de server.
browser server
Cookie: session=a3f9c1 ──► sessie a3f9c1 = gebruiker 42, ingelogd om 09:14, rol: student
| Session met server-opslag | Token (bv. JWT) | |
|---|---|---|
| Wat de client bijhoudt | Enkel een id | De gegevens zelf, ondertekend |
| Waar de gegevens staan | Op de server | In het token |
| Ongeldig maken | Wissen op de server, meteen weg | Moeilijker: het token blijft geldig tot het vervalt |
| Meegestuurd via | Cookie-header, automatisch | Authorization-header, door jouw code |
Je hoeft dit nu niet zelf te bouwen. Wat je moet kunnen uitleggen: HTTP onthoudt niets, dus elk request draagt zelf het bewijs van wie je bent, via een cookie of via een token.
Same-Origin Policy
Nu het gevolg van dat automatisme. Omdat je browser cookies vanzelf meestuurt, zou elke website die je bezoekt in jouw naam gegevens kunnen opvragen bij je bank, je mailbox of je schoolplatform, en het antwoord uitlezen. Daarom leggen browsers zichzelf een grens op: de Same-Origin Policy.
Een origin bestaat uit drie delen. Alle drie moeten gelijk zijn.
https://app.voorbeeld.be:443
↑ ↑ ↑
scheme host poort
Vergeleken met https://app.voorbeeld.be:
| URL | Zelfde origin? | Waarom |
|---|---|---|
https://app.voorbeeld.be/api/v2 | Ja | Enkel het pad verschilt, en dat telt niet mee |
http://app.voorbeeld.be | Nee | Ander scheme |
https://api.voorbeeld.be | Nee | Andere host, ook al is het hetzelfde domein |
https://app.voorbeeld.be:8443 | Nee | Andere poort |
De regel: JavaScript op de ene origin mag standaard de response van een andere origin niet lezen.
Precies daarom werkt curl https://api.voorbeeld.be/producten altijd, terwijl dezelfde aanroep vanuit je pagina geblokkeerd wordt. De Same-Origin Policy beschermt de gebruiker in zijn browser, niet de server.
CORS: de server geeft toestemming
CORS (Cross-Origin Resource Sharing) is de manier waarop een server zegt: deze andere origin mag mijn antwoord wel lezen. Dat gebeurt met één response header:
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Origin: *
Klik door wat er gebeurt bij een cross-origin verzoek:
Stap 1. Je code stuurt een fetch()
Een pagina op de ene origin vraagt data op bij een andere.
Je pagina draait op http://localhost:5173 en wil gegevens ophalen bij https://api.voorbeeld.be. Scheme, host en poort verschillen alle drie, dus dit is een cross-origin verzoek. Voor jouw code ziet het er nochtans uit als elk ander verzoek.
fetch('https://api.voorbeeld.be/producten')De headers op een rij
| Header | Wie stuurt hem | Wat hij zegt |
|---|---|---|
Origin | Browser, automatisch | Vanaf welke site het verzoek komt |
Access-Control-Allow-Origin | Server | Welke origin de response mag lezen (* of één adres) |
Access-Control-Allow-Methods | Server, bij preflight | Welke methoden toegelaten zijn |
Access-Control-Allow-Headers | Server, bij preflight | Welke eigen headers je mag meesturen |
Access-Control-Allow-Credentials | Server | Of cookies meegestuurd mogen worden |
Access-Control-Max-Age | Server | Hoelang het preflight-antwoord hergebruikt mag worden |
* en cookies gaan niet samenMet Access-Control-Allow-Origin: * mag iedereen lezen, maar dan weigert de browser cookies mee te sturen. Wil je een sessie over origins heen, dan moet de server één concrete origin noemen én Access-Control-Allow-Credentials: true zetten, en jij credentials: 'include' meegeven aan fetch().
De fout herkennen en oplossen
Access to fetch at 'https://api.voorbeeld.be/producten' from origin
'http://localhost:5173' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.
Drie dingen die je meteen weet als je dit ziet:
- Het probleem zit niet in je JavaScript, je URL of je JSON.
- Het request is uitgevoerd: in de Network tab staat het er, vaak met
200. - De oplossing ligt bij de server, niet bij je frontend.
| Situatie | Wat je doet |
|---|---|
| De API is van jou | Zet de juiste Access-Control-Allow-Origin in je serverconfiguratie of middleware |
| De API is van iemand anders | Roep ze aan vanaf je eigen server; die heeft geen last van CORS |
| Enkel tijdens ontwikkeling | Gebruik de proxy van je dev server, zodat alles vanuit één origin lijkt te komen |
Zo'n dev-proxy ziet er in Vite zo uit: verzoeken naar /api gaan naar de echte API, maar je browser ziet enkel je eigen origin.
// vite.config.js
export default {
server: {
proxy: { '/api': 'https://api.voorbeeld.be' },
},
};
Een browserextensie installeren die CORS uitschakelt, of --disable-web-security gebruiken. Het probleem verdwijnt dan enkel op jouw machine: je bezoekers krijgen de fout gewoon. Bovendien zet je daarmee de beveiliging uit voor élke site die je daarna bezoekt.
Dezelfde fetch(), twee keer naast elkaar. Let op waar het rechts misloopt: niet bij de server, die antwoordt gewoon, maar bij de browser, die de response niet aan je code doorgeeft.
Test jezelf
1. Je `fetch()` wordt geblokkeerd door CORS. Wat is er met het request gebeurd?
2. Welke response header stuurt dit gedrag?
3. Je pagina draait op `https://app.voorbeeld.be` en roept `https://api.voorbeeld.be` aan. Is dat cross-origin?
4. Waarom blijf je ingelogd terwijl HTTP stateless is?
- HTTP is stateless: elk request draagt zelf het bewijs van wie je bent, via een cookie of een token.
- De server zet een cookie met
Set-Cookie; de browser stuurt het daarna automatisch terug metCookie. HttpOnly,SecureenSameSitebepalen hoe goed dat cookie beschermd is.- Een origin is scheme + host + poort; alle drie moeten gelijk zijn.
- De Same-Origin Policy is een regel van de browser: het request vertrekt wél, maar je code krijgt de response niet.
- CORS heft die regel op via
Access-Control-Allow-Origin; de oplossing ligt dus altijd bij de server, of bij een proxy.