Zoeken op website

Hoe loginverificatie toe te voegen aan React-applicaties


De auteur koos voor Creative Commons om een donatie te ontvangen als onderdeel van het Write for Donations-programma.

Inleiding

Authenticatie in React-toepassingen beheert de toegang van gebruikers tot beschermde pagina's en bronnen. Wanneer een gebruiker inlogt, slaat je toepassing de authenticatiegegevens op (meestal een token) en controleert deze voordat privé-routes worden weergegeven. De authenticatiestatus moet behouden blijven bij het verversen van pagina's, wat vereist dat tokens in de browseropslag worden opgeslagen in plaats van in de componentstatus.

Hoe de authenticatiestatus behouden blijft: Wanneer je tokens opslaat in localStorage of sessionStorage, overleven ze pagina-verversingen en het herstarten van de browser (voor localStorage). Dit betekent dat je app bij het laden kan controleren op een bestaand token en gebruikers automatisch kan authenticeren zonder dat een nieuwe login vereist is. Zonder persistente opslag zouden gebruikers iedere keer opnieuw moeten inloggen bij het verversen van de pagina of het openen van een nieuw tabblad.

Deze tutorial legt stap voor stap uit hoe je een React-applicatie bouwt met tokengebaseerde authenticatie, waarbij React Router v6 wordt gebruikt voor beveiligde routes. Je maakt een mock-API die authenticatietokens retourneert, implementeert een inlogpagina die tokens ophaalt en opslaat, en stelt authenticatiecontroles in die de bedoelde route van de gebruiker behouden. De tutorial behandelt verschillende methoden voor het opslaan van tokens (localStorage, sessionStorage) en hun beveiligingsimplicaties, implementeert functies voor uitloggen en foutafhandeling, en laat zien hoe je JWT-tokens decodeert om de vervaldatum te controleren.

Waarom authenticatielogica niet in afzonderlijke componenten moet staan: Het centraliseren van authenticatielogica in een aangepaste hook of React Context zorgt voor consistent gedrag in je app, maakt het gemakkelijker om authenticatiestromen bij te werken en voorkomt duplicatie van code. Wanneer authenticatielogica verspreid is over componenten, loop je het risico op inconsistente tokencontroles, gemiste randgevallen en onderhoudsproblemen.

Na het voltooien van deze tutorial zul je begrijpen hoe je veilige authenticatie kunt implementeren in React-toepassingen, geschikte strategieën voor het opslaan van tokens kunt kiezen op basis van beveiligings- en gebruikerseisen, en veelvoorkomende authenticatiescenario's kunt afhandelen, waaronder beveiligde routes, tokenverval en uitloggen.

Moet je snel een React-project implementeren? Bekijk het Linux-Console.net App Platform en zet een React-project rechtstreeks vanaf GitHub binnen enkele minuten live.

Belangrijkste punten

  • De authenticatiestatus blijft behouden bij het vernieuwen van de pagina wanneer deze wordt opgeslagen in localStorage of sessionStorage. Tokens die in de componentstatus (useState) worden opgeslagen, gaan verloren bij het vernieuwen, waardoor gebruikers opnieuw moeten inloggen. Browseropslag-API's behouden tokens tussen sessies, waardoor automatische opnieuw-authenticatie mogelijk is.

  • React Router v6 gebruikt Routes in plaats van Switch en vereist element-props in plaats van children. De nieuwe API is meer declaratief en typeveilig. Beschermde routes worden geïmplementeerd met een wrappercomponent die de authenticatie controleert voordat deze wordt weergegeven.

  • Centraleer de authenticatielogica in een custom hook of React Context. Het bewaren van auth-logic in individuele componenten leidt tot duplicatie en inconsistent gedrag. Een custom useToken hook of AuthContext biedt een enkele bron van waarheid voor de authenticatiestatus.

  • localStorage blijft behouden tussen browsersessies; sessionStorage is specifiek voor een tab. Gebruik localStorage voor de "onthoud mij"-functionaliteit en sessionStorage voor veiligere, sessie-only authenticatie. Beide zijn kwetsbaar voor XSS-aanvallen, dus maak altijd gebruikersinvoer schoon.

  • JWT-tokens kunnen aan de clientzijde worden gedecodeerd om de vervaldatum te controleren zonder een serverronde. De payload bevat een exp-claim (Unix-tijdstempel) die je kunt vergelijken met de huidige tijd. Valideer tokens echter altijd op de server, aangezien controles aan de clientzijde alleen voor de gebruikerservaring zijn.

  • CSRF-bescherming is cruciaal bij het gebruik van op cookies gebaseerde authenticatie. Cookies worden automatisch meegestuurd met verzoeken, waardoor ze kwetsbaar zijn voor cross-site request forgery. Gebruik CSRF-tokens, SameSite-cookie-attributen en verifieer de Origin-header op de server.

Vereisten

  • Een ontwikkelomgeving met Node.js versie 18.x en npm versie 9.x. Om Node.js op macOS of Ubuntu te installeren, volg de stappen in Hoe Node.js te installeren en een lokale ontwikkelomgeving op macOS te maken of Hoe Node.js te installeren op Ubuntu.

  • Een React-ontwikkelomgeving opgezet met Create React App, waarbij de niet-essentiële sjablooncode is verwijderd. Volg hiervoor Stap 1 — Een leeg project maken van de tutorial Hoe je State beheert in React Class Components. In deze tutorial wordt auth-tutorial als projectnaam gebruikt.

  • Bekendheid met het ophalen van gegevens van API's in React. Voor meer informatie over het werken met API's, zie Hoe je Web-API's aanroept met de useEffect Hook in React.

  • Basiskennis van JavaScript, HTML en CSS. Leer meer in onze serie Hoe je een website bouwt met HTML, Hoe je HTML stijlt met CSS, en Hoe je codeert in JavaScript.

Stap 1 — Een inlogpagina maken

Loginpagina's onderscheppen niet-geauthenticeerde gebruikers voordat ze beschermde routes bereiken. In plaats van gebruikers om te leiden naar een aparte loginroute (waardoor hun bedoelde bestemming verloren gaat), toon het loginformulier op elke route wanneer er geen authenticatietoken aanwezig is. Dit behoudt de navigatie-intentie van de gebruiker om /dashboard te bezoeken terwijl ze zijn uitgelogd, toont het loginformulier, en na authenticatie komen ze op /dashboard terecht in plaats van ergens anders te worden omgeleid.

Waarom de login op een willekeurige route weergeven: Wanneer gebruikers een beschermde route zoals /dashboard bezoeken terwijl ze niet zijn ingelogd, zorgt het tonen van het loginformulier op diezelfde route ervoor dat ze hun navigatiecontext behouden. Na het inloggen bevinden ze zich al op hun bedoelde bestemming zonder extra doorgestuurde stappen. Dit zorgt voor een soepelere gebruikerservaring in vergelijking met het doorsturen naar /login en vervolgens nog een extra navigatiestap vereisen.

Installeer React Router met npm. Er zijn twee versies: een webversie voor React-toepassingen en een native versie voor React Native. Installeer de webversie:

npm install react-router-dom

De installatie wordt voltooid met een uitvoer die vergelijkbaar is met:

...
+ react-router-dom@6.28.0
added 11 packages from 6 contributors, removed 10 packages and audited 1945 packages in 12.794s
...

Maak vervolgens twee componenten genaamd Dashboard en Preferences aan om als privépagina's te functioneren. Deze zullen componenten vertegenwoordigen die een gebruiker niet zou moeten zien totdat hij succesvol is ingelogd in de applicatie.

Maak eerst de mappen aan:

mkdir src/components/Dashboard

mkdir src/components/Preferences

Open Dashboard.js in een teksteditor:

nano src/components/Dashboard/Dashboard.js

Voeg binnen Dashboard.js een <h2>-tag toe met de inhoud van Dashboard:

import React from 'react';

export default function Dashboard() {
  return(
    <h2>Dashboard</h2>
  );
}

Sla het bestand op en sluit het.

Herhaal dezelfde stappen voor Voorkeuren. Open het onderdeel:

nano src/components/Preferences/Preferences.js

Voeg de inhoud toe:

import React from 'react';

export default function Preferences() {
  return(
    <h2>Preferences</h2>
  );
}

Sla het bestand op en sluit het.

Nu je enkele componenten hebt, moet je de componenten importeren en routes maken in App.js. Bekijk de tutorial Hoe Routing in React Apps met React Router te Beheren voor een volledige introductie tot routing in React-toepassingen. Voor het implementeren van je React-toepassing, zie Hoe een React-toepassing te Implementeren met Nginx op Ubuntu.

Om te beginnen, open App.js:

nano src/components/App/App.js

Importeer vervolgens Dashboard en Preferences door de volgende gemarkeerde code toe te voegen:

import React from 'react';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';

function App() {
  return (
    <></>
  );
}

export default App;

Importeer vervolgens BrowserRouter, Routes en Route uit react-router-dom. React Router v6 gebruikt Routes in plaats van Switch, en routes gebruiken de element-prop in plaats van children:

import React from 'react';
import './App.css';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';

function App() {
  return (
    <></>
  );
}

export default App;

Voeg een omliggende <div> toe met een className van wrapper en een <h1> tag om als sjabloon voor de applicatie te dienen. Zorg ervoor dat je App.css importeert zodat je de stijlen kunt toepassen.

Maak vervolgens routes voor de Dashboard en Preferences componenten. Voeg BrowserRouter toe en daarna een Routes component als kind. Voeg binnen de Routes een Route toe met een path en element prop voor elk component:

import React from 'react';
import './App.css';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';

function App() {
  return (
    <div className="wrapper">
      <h1>Application</h1>
      <BrowserRouter>
        <Routes>
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/preferences" element={<Preferences />} />
        </Routes>
      </BrowserRouter>
    </div>
  );
}

export default App;

Sla het bestand op en sluit het.

De laatste stap is het toevoegen van wat opvulling aan de hoofd<div>, zodat je component niet direct tegen de rand van de browser aan staat. Hiervoor ga je de CSS aanpassen.

Open App.css:

nano src/components/App/App.css

Vervang de inhoud door een klasse .wrapper met padding van 20px:

.wrapper {
    padding: 20px;
}

Sla het bestand op en sluit het. De browser wordt automatisch opnieuw geladen en toont de basiscomponenten:

Bezoek http://localhost:3000/dashboard om de dashboardpagina te bekijken:

De routes werken, maar /dashboard en /preferences zijn momenteel voor iedereen toegankelijk, ook voor niet-ingelogde gebruikers. Dit zouden beveiligde routes moeten zijn.

Twee benaderingen om routes te beveiligen:

  1. Omleiden naar aparte login-route: Maak een /login route aan en leid niet-ingelogde gebruikers daar naartoe. Dit werkt, maar gebruikers verliezen hun bedoelde bestemming en moeten na het inloggen terug navigeren.

  2. Loginformulier weergeven op elke route (gebruikt in deze tutorial): Toon het loginformulier op de huidige route wanneer er geen token bestaat. Na authenticatie blijven gebruikers op hun bedoelde bestemming. Bijvoorbeeld, het bezoeken van /dashboard terwijl je bent uitgelogd, toont het loginformulier, en na het inloggen komen gebruikers direct op /dashboard terecht.

Deze handleiding gebruikt de tweede aanpak voor een betere gebruikerservaring.

Om te beginnen, maak een nieuwe map voor de Login-component:

mkdir src/components/Login

Open vervolgens Login.js in een teksteditor:

nano src/components/Login/Login.js

Maak een eenvoudig formulier met een verzend <button> en een <input> voor de gebruikersnaam en het wachtwoord. Zorg ervoor dat je het invoertype voor het wachtwoord instelt op password:

import React from 'react';

export default function Login() {
  return(
    <form>
      <label>
        <p>Username</p>
        <input type="text" />
      </label>
      <label>
        <p>Password</p>
        <input type="password" />
      </label>
      <div>
        <button type="submit">Submit</button>
      </div>
    </form>
  )
}

Voor meer informatie over formulieren in React, bekijk de tutorial Hoe je formulieren bouwt in React. Om te leren over het beheren van state in React-componenten, zie Hoe je state beheert met Hooks in React-componenten.

Voeg vervolgens een <h1>-tag toe waarin de gebruiker wordt gevraagd in te loggen. Wikkel het <form> en het <h1> in een <div> met een className van login-wrapper. Importeer ten slotte Login.css:

import React from 'react';
import './Login.css';

export default function Login() {
  return(
    <div className="login-wrapper">
      <h1>Please Log In</h1>
      <form>
        <label>
          <p>Username</p>
          <input type="text" />
        </label>
        <label>
          <p>Password</p>
          <input type="password" />
        </label>
        <div>
          <button type="submit">Submit</button>
        </div>
      </form>
    </div>
  )
}

Sla het bestand op en sluit het.

Voeg opmaak toe om het loginformulier te centreren. Open Login.css:

nano src/components/Login/Login.css

Centreer de component op de pagina door een display van flex toe te voegen, vervolgens de flex-direction in te stellen op column om de elementen verticaal uit te lijnen en align-items op center te zetten om de component in de browser te centreren:

.login-wrapper {
    display: flex;
    flex-direction: column;
    align-items: center;
}

Voor meer informatie over het gebruik van Flexbox, zie onze CSS Flexbox Spiekbrief

Sla het bestand op en sluit het.

Geef de logincomponent weer in App.js wanneer er geen authenticatietoken aanwezig is. Open App.js:

nano src/components/App/App.js

In Stap 3 ga je het blijvend opslaan van tokens implementeren. Sla voorlopig het token op in de componentstatus met de useState Hook om de loginflow te testen. Let op: tokens die in de componentstatus zijn opgeslagen, gaan verloren bij het verversen van de pagina.

Importeer useState van react, roep vervolgens useState aan en stel de teruggegeven waarden in op token en setToken:

import React, { useState } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Preferences from '../Preferences/Preferences';

function App() {
  const [token, setToken] = useState();
  return (
    <div className="wrapper">
      <h1>Application</h1>
      <BrowserRouter>
        <Routes>
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/preferences" element={<Preferences />} />
        </Routes>
      </BrowserRouter>
    </div>
  );
}

export default App;

Importeer de Login-component. Voeg een voorwaardelijke instructie toe om Login weer te geven als het token niet waarheidsgetrouw is.

Geef de setToken functie door aan de Login component:

import React, { useState } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';

import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';

function App() {
  const [token, setToken] = useState();

  if(!token) {
    return <Login setToken={setToken} />
  }

  return (
    <div className="wrapper">
      <h1>Application</h1>
      <BrowserRouter>
        <Routes>
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/preferences" element={<Preferences />} />
        </Routes>
      </BrowserRouter>
    </div>
  );
}

export default App;

Omdat er nog geen token bestaat, wordt het loginformulier op alle routes weergegeven. In de volgende stap zul je een API-endpoint maken dat een token terugstuurt na succesvolle authenticatie.

Sla het bestand op en sluit het. De browser wordt automatisch opnieuw geladen en toont de inlogpagina op alle routes. Bezoek de dashboardpagina om te controleren of het inlogformulier verschijnt:

Je hebt een loginformulier gemaakt dat op alle routes wordt weergegeven wanneer er geen authenticatietoken aanwezig is. Het formulier voorkomt toegang tot beschermde routes zoals /dashboard en /preferences totdat de authenticatie slaagt. In de volgende stap ga je een mock API-server maken die authenticatietokens teruggeeft.

Stap 2 — Een Token-API maken

Mock-API's simuleren backend-authenticatie tijdens de ontwikkeling zonder een productieserver te vereisen. Express biedt een eenvoudige manier om een ontwikkelserver te maken die tokens retourneert in hetzelfde formaat dat uw productie-API zal gebruiken. Hiermee kunt u authenticatiestromen testen, API-responses verwerken en de frontend onafhankelijk ontwikkelen.

Waarom een mock-API gebruiken: Tijdens de ontwikkeling heb je mogelijk nog geen backend klaar, of je wilt authenticatiestromen testen zonder een productieserver te benaderen. Een mock-API geeft voorspelbare reacties terug, wat het makkelijker maakt om foutafhandeling, laadstatussen en tokenbeheer te testen. Voor productietoepassingen vervang je de mock-API door je daadwerkelijke authenticatie-eindpunt.

Maak een eenvoudige Express-server die een token retourneert. Voor een gedetailleerde introductie tot Express, zie Basis Express-server in Node.js. Als je een volledige stack-applicatie bouwt, overweeg dan Hoe je een To-Do-applicatie bouwt met Django en React voor een volledig voorbeeld.

Om te beginnen, installeer express. Aangezien de server geen vereiste is voor de uiteindelijke build, zorg ervoor dat je het installeert als een devDependency.

Je moet ook cors installeren. Deze bibliotheek maakt cross-origin resource sharing mogelijk voor alle routes.

Waarschuwing: Schakel CORS niet in voor alle routes in een productieapplicatie. Dit kan leiden tot beveiligingsrisico's.

npm install --save-dev express cors

De installatie wordt voltooid met een uitvoer die vergelijkbaar is met:

...
+ cors@2.8.5
+ express@4.17.1
removed 10 packages, updated 2 packages and audited 2059 packages in 12.597s
...

Open vervolgens een nieuw bestand genaamd server.js in de hoofdmap van je applicatie. Voeg dit bestand niet toe aan de /src-map, omdat je niet wilt dat het deel uitmaakt van de uiteindelijke build.

nano server.js

Importeer express en initialiseer vervolgens een nieuwe app door express() aan te roepen en het resultaat op te slaan in een variabele genaamd app:

const express = require('express');
const app = express();

Na het aanmaken van de app, voeg cors toe als middleware. Importeer eerst cors, en voeg het daarna toe aan de applicatie door de use-methode op app aan te roepen:

const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors());

Voeg een routehandler toe voor /login die een token retourneert. De handler ontvangt req (verzoekgegevens) en res (response-object). Gebruik res.send om een JavaScript-object terug te sturen met een token:

const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors());

app.use('/login', (req, res) => {
  res.send({
    token: 'test123'
  });
});

Voer ten slotte de server uit op poort 8080 met app.listen:

const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors());

app.use('/login', (req, res) => {
  res.send({
    token: 'test123'
  });
});

app.listen(8080, () => console.log('API is running on http://localhost:8080'));

Sla het bestand op en sluit het. Start de server in een nieuw terminalvenster of tabblad:

node server.js

De server start en toont:

API is running on http://localhost:8080/login

Bezoek de inlogpagina in je browser om de JSON-objectrespons te bekijken:

Opmerking: Het bezoeken van de URL in een browser stuurt een GET-verzoek, terwijl het loginformulier een POST-verzoek zal sturen. De huidige route-handler accepteert beide methoden. In productie, beperk routes tot specifieke HTTP-methoden door app.post() te gebruiken voor POST-verzoeken en app.get() voor GET-verzoeken. Zie de Express-routeringsdocumentatie voor details.

Nu je een werkende API-server hebt, moet je een verzoek doen vanaf je loginpagina. Open Login.js:

nano src/components/Login/Login.js

In de vorige stap heb je een nieuwe prop genaamd setToken doorgegeven aan de Login-component. Voeg de PropType van de nieuwe prop toe en destructureer het props-object om de setToken-prop eruit te halen.

import React from 'react';
import PropTypes from 'prop-types';

import './Login.css';

export default function Login({ setToken }) {
  return(
    <div className="login-wrapper">
      <h1>Please Log In</h1>
      <form>
        <label>
          <p>Username</p>
          <input type="text" />
        </label>
        <label>
          <p>Password</p>
          <input type="password" />
        </label>
        <div>
          <button type="submit">Submit</button>
        </div>
      </form>
    </div>
  )
}

Login.propTypes = {
  setToken: PropTypes.func.isRequired
}

Maak vervolgens een lokale status aan om de Username en Password vast te leggen. Aangezien je de gegevens niet handmatig hoeft in te stellen, maak je van de <inputs> ongecontroleerde componenten. Je kunt gedetailleerde informatie over ongecontroleerde componenten vinden in Hoe je formulieren bouwt in React.

import React, { useState } from 'react';
import PropTypes from 'prop-types';

import './Login.css';

export default function Login({ setToken }) {
  const [username, setUserName] = useState();
  const [password, setPassword] = useState();
  return(
    <div className="login-wrapper">
      <h1>Please Log In</h1>
      <form>
        <label>
          <p>Username</p>
          <input type="text" onChange={e => setUserName(e.target.value)}/>
        </label>
        <label>
          <p>Password</p>
          <input type="password" onChange={e => setPassword(e.target.value)}/>
        </label>
        <div>
          <button type="submit">Submit</button>
        </div>
      </form>
    </div>
  )
}

Login.propTypes = {
  setToken: PropTypes.func.isRequired
};

Maak een functie om een POST-verzoek naar de server te sturen. In grotere toepassingen plaats je API-aanroepen in een aparte servicedirectory. Voor deze tutorial is de functie gedefinieerd in het componentbestand. Voor meer informatie over API-aanroepen in React, zie Hoe Web-API's aan te roepen met de useEffect Hook in React.

Maak een async functie genaamd loginUser. De functie neemt credentials als argument, en zal vervolgens de fetch-methode aanroepen met de POST-optie:

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './Login.css';

async function loginUser(credentials) {
 return fetch('http://localhost:8080/login', {
   method: 'POST',
   headers: {
     'Content-Type': 'application/json'
   },
   body: JSON.stringify(credentials)
 })
   .then(data => data.json())
}

export default function Login({ setToken }) {
...

Maak ten slotte een formulierverzendhandler genaamd handleSubmit die loginUser aanroept met de username en password. Roep setToken aan bij een succesvol resultaat. Roep handleSubmit aan met de onSubmit event handler op het <form>:

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './Login.css';

async function loginUser(credentials) {
 return fetch('http://localhost:8080/login', {
   method: 'POST',
   headers: {
     'Content-Type': 'application/json'
   },
   body: JSON.stringify(credentials)
 })
   .then(data => data.json())
}

export default function Login({ setToken }) {
  const [username, setUserName] = useState();
  const [password, setPassword] = useState();

  const handleSubmit = async e => {
    e.preventDefault();
    const token = await loginUser({
      username,
      password
    });
    setToken(token);
  }

  return(
    <div className="login-wrapper">
      <h1>Please Log In</h1>
      <form onSubmit={handleSubmit}>
        <label>
          <p>Username</p>
          <input type="text" onChange={e => setUserName(e.target.value)} />
        </label>
        <label>
          <p>Password</p>
          <input type="password" onChange={e => setPassword(e.target.value)} />
        </label>
        <div>
          <button type="submit">Submit</button>
        </div>
      </form>
    </div>
  )
}

Login.propTypes = {
  setToken: PropTypes.func.isRequired
};

Opmerking: In productieapplicaties moet u het ontkoppelen van componenten tijdens asynchrone bewerkingen afhandelen. Als een gebruiker navigeert voordat een Promise is opgelost, moet u mogelijk het verzoek annuleren of de reactie negeren. Zie Hoe web-API's aan te roepen met de useEffect Hook in React voor opruimingspatronen.

Sla het bestand op en sluit het. Zorg ervoor dat de API-server nog steeds draait en ga vervolgens in je browser naar http://localhost:3000/dashboard.

De inlogpagina verschijnt. Voer een willekeurige gebruikersnaam en wachtwoord in en verstuur dan het formulier. De API geeft een token terug en de app gaat naar het dashboard.

De loginstroom werkt: het indienen van het formulier roept de API aan en slaat het geretourneerde token op. Het token wordt echter opgeslagen in de componentstatus (JavaScript-geheugen), wat betekent dat het verloren gaat bij het verversen van de pagina of bij het openen van een nieuw tabblad. Gebruikers moeten na elke verversing opnieuw inloggen.

Het probleem: De status van een component (useState) blijft niet behouden bij het vernieuwen van de pagina. Wanneer de browser wordt vernieuwd, initialiseert React de componenten opnieuw en gaat alle in het geheugen opgeslagen status verloren.

In de volgende stap ga je blijvende tokenopslag implementeren met behulp van browseropslag-API's (localStorage of sessionStorage), zodat authenticatie behouden blijft bij het verversen van de pagina en tijdens browsersessies.

Stap 3 — Een gebruikers-token opslaan met sessionStorage en localStorage

De opslaglocatie van tokens bepaalt de sessiepersistentie en de beveiligingsoverwegingen. localStorage houdt gebruikers ingelogd over verschillende browserversies en tabbladen, terwijl sessionStorage wordt gewist zodra het tabblad wordt gesloten. Beide zijn toegankelijk voor JavaScript, waardoor ze kwetsbaar zijn voor XSS-aanvallen, maar ze bieden een betere gebruikerservaring dan het alleen opslaan van tokens in de componentstatus.

Vergelijking van opslagopties:

  • Componentstatus (useState): Verloren bij het verversen van de pagina. Geen persistentie.
  • sessionStorage: Blijft bestaan voor de sessie van het browsertabblad. Wordt gewist wanneer het tabblad wordt gesloten. Niet gedeeld tussen tabbladen.
  • localStorage: Blijft behouden tussen browser-sessies en tabbladen. Overleeft het opnieuw starten van de browser. Wordt gedeeld tussen alle tabbladen van dezelfde oorsprong.
  • Cookies: Worden automatisch meegezonden met verzoeken. Vereisen CSRF-bescherming. Kunnen httpOnly zijn (niet toegankelijk voor JavaScript, veiliger).

Beveiligingsoverweging: Alle gegevens die buiten het geheugengebied van de component worden opgeslagen (inclusief localStorage, sessionStorage en cookies die toegankelijk zijn voor JavaScript) zijn kwetsbaar voor Cross-Site Scripting (XSS)-aanvallen. Als kwaadwillige code op uw domein wordt uitgevoerd, kan deze toegang krijgen tot deze opslagmechanismen. Sanitize altijd gebruikersinvoer en gebruik Content Security Policy (CSP)-headers om XSS-risico's te beperken.

Deze tutorial behandelt sessionStorage en localStorage als moderne alternatieven voor cookies voor het opslaan van tokens. Cookies worden besproken in Stap 8 bij het behandelen van CSRF-bescherming.

Sessieopslag

Om de voordelen van opslag buiten het geheugen te testen, converteer de opslag in het geheugen naar sessionStorage. Open App.js:

nano src/components/App/App.js

Verwijder de aanroep naar useState en maak twee nieuwe functies aan genaamd setToken en getToken. Roep vervolgens getToken aan en wijs de resultaten toe aan een variabele genaamd token:

import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';

import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';

function setToken(userToken) {
}

function getToken() {
}

function App() {
  const token = getToken();

  if(!token) {
    return <Login setToken={setToken} />
  }

  return (
    <div className="wrapper">
      <h1>Application</h1>
      <BrowserRouter>
        <Routes>
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/preferences" element={<Preferences />} />
        </Routes>
      </BrowserRouter>
    </div>
  );
}

export default App;

Het gebruik van dezelfde functienamen (setToken, getToken) en variabelenamen (token) betekent dat de Login-component en de rest van App.js geen wijzigingen nodig hebben. De interface blijft hetzelfde, maar de implementatie maakt nu gebruik van persistente opslag.

Implementeer setToken om het token op te slaan in sessionStorage. De setItem-methode vereist een sleutel (string) en een waarde (string), dus zet het userToken-object om in een JSON-string met JSON.stringify:

import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';

import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';

function setToken(userToken) {
  sessionStorage.setItem('token', JSON.stringify(userToken));
}

function getToken() {
}

function App() {
  const token = getToken();

  if(!token) {
    return <Login setToken={setToken} />
  }

  return (
    <div className="wrapper">
      <h1>Application</h1>
      <BrowserRouter>
        <Routes>
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/preferences" element={<Preferences />} />
        </Routes>
      </BrowserRouter>
    </div>
  );
}

export default App;

Sla het bestand op. De browser wordt automatisch opnieuw geladen. Nadat je het inlogformulier hebt verzonden, wordt het token opgeslagen in sessionStorage, maar de inlogpagina wordt nog steeds weergegeven omdat React nog niet opnieuw heeft gerenderd. Open de ontwikkelaarstools van je browser (F12) en controleer het tabblad Opslag om te verifiëren dat het token is opgeslagen. De volgende afbeelding toont de ontwikkelaarstools van Firefox; Chrome en andere moderne browsers hebben vergelijkbare opslaginspectietools.

Implementeer getToken om het token op te halen uit sessionStorage. De getItem-methode geeft de opgeslagen string terug (of null als er geen waarde bestaat). Parse de JSON-string terug naar een object en retourneer vervolgens de token-eigenschap:

import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';

function setToken(userToken) {
  sessionStorage.setItem('token', JSON.stringify(userToken));
}

function getToken() {
  const tokenString = sessionStorage.getItem('token');
  if (!tokenString) {
    return null;
  }
  const userToken = JSON.parse(tokenString);
  return userToken?.token;
}

function App() {
  const token = getToken();

  if(!token) {
    return <Login setToken={setToken} />
  }

  return (
    <div className="wrapper">
      <h1>Application</h1>
      <BrowserRouter>
        <Routes>
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/preferences" element={<Preferences />} />
        </Routes>
      </BrowserRouter>
    </div>
  );
}

export default App;

Waarom optionele chaining gebruiken (?.): Bij het eerste bezoek geeft sessionStorage.getItem('token') null terug, en JSON.parse(null) geeft null terug. Zonder optionele chaining veroorzaakt het toegang krijgen tot userToken.token wanneer userToken null is een TypeError. De ?. operator geeft veilig undefined terug in plaats van een fout te gooien.

Sla het bestand op en sluit het. Als er een token bestaat in sessionStorage, navigeert de browser bij verversen automatisch naar de beveiligde pagina's:

Om de loginprocedure opnieuw te testen, wis je de opgeslagen token. Verwijder in de ontwikkelaarstools van je browser de token in het Storage-tabblad of voer sessionStorage.clear() uit in de console.

Het probleem: Na het inloggen wordt het token opgeslagen in sessionStorage, maar de loginpagina wordt nog steeds weergegeven. Dit gebeurt omdat React niet weet dat het token is opgeslagen, en de component moet opnieuw renderen wanneer het token verandert.

Waarom React niet opnieuw rendert: Het opslaan van gegevens in sessionStorage veroorzaakt geen her-rendering in React. React rendert alleen opnieuw wanneer de state verandert. Om dit op te lossen, gebruik je React state om de token bij te houden en werk je die state bij wanneer je tokens opslaat of ophaalt. Dit zorgt voor opnieuw renderen en werkt de UI bij.

Oplossing: Maak een aangepaste Hook die opslagbewerkingen combineert met React-state. Dit centraliseert de logica voor tokenbeheer en maakt het herbruikbaar in verschillende componenten.

Een aangepaste tokenhook maken

Wat is een custom Hook: Een custom Hook is een JavaScript-functie die React Hooks gebruikt (zoals useState, useEffect) en herbruikbare logica inkapselt. Volgens de conventie beginnen custom Hooks met use* (bijv. useToken, useAuth). Custom Hooks stellen je in staat componentlogica uit te trekken naar herbruikbare functies, waardoor de code beter onderhoudbaar en testbaar wordt.

Open een nieuw bestand in de App map genaamd useToken.js:

nano src/components/App/useToken.js

Bestandsorganisatie: Deze Hook kan direct worden gedefinieerd in App.js, maar het extraheren naar een apart bestand laat zien hoe aangepaste Hooks onafhankelijk van componenten werken. Naarmate je applicatie groeit en je deze Hook in meerdere componenten hergebruikt, maakt het in een apart bestand houden het gemakkelijker om te onderhouden en te testen.

Importeer in useToken.js useState van react. Aangezien dit bestand geen JSX bevat, hoef je React niet te importeren. Maak en exporteer een functie genaamd useToken die useState gebruikt om de tokenstatus te beheren:

import { useState } from 'react';

export default function useToken() {
  const [token, setToken] = useState();

}

Voeg de functie getToken toe als een pijlfunctie binnen useToken. Plaats dit vóór de declaratie van de status en initialiseer vervolgens useState met getToken() om een bestaand token te laden wanneer de component wordt geactiveerd:

import { useState } from 'react';

export default function useToken() {
  const getToken = () => {
    const tokenString = sessionStorage.getItem('token');
    if (!tokenString) {
      return null;
    }
    const userToken = JSON.parse(tokenString);
    return userToken?.token;
  };
  const [token, setToken] = useState(getToken());

}

Voeg een saveToken-functie toe die de token opslaat in zowel sessionStorage als de React-state. Opslaan in de state veroorzaakt een her-render, waardoor de UI wordt bijgewerkt:

import { useState } from 'react';

export default function useToken() {
  const getToken = () => {
    const tokenString = sessionStorage.getItem('token');
    if (!tokenString) {
      return null;
    }
    const userToken = JSON.parse(tokenString);
    return userToken?.token;
  };

  const [token, setToken] = useState(getToken());

  const saveToken = userToken => {
    sessionStorage.setItem('token', JSON.stringify(userToken));
    setToken(userToken.token);
  };
}

Geef een object terug met token en setToken (gekoppeld aan saveToken). Het gebruik van een object in plaats van een array stelt componenten in staat om alleen de waarden te ontleden die ze nodig hebben. Dit behoudt dezelfde interface als voorheen, dus App.js hoeft niet te worden aangepast:

import { useState } from 'react';

export default function useToken() {
  const getToken = () => {
    const tokenString = sessionStorage.getItem('token');
    if (!tokenString) {
      return null;
    }
    const userToken = JSON.parse(tokenString);
    return userToken?.token;
  };

  const [token, setToken] = useState(getToken());

  const saveToken = userToken => {
    sessionStorage.setItem('token', JSON.stringify(userToken));
    setToken(userToken.token);
  };

  return {
    setToken: saveToken,
    token
  }
}

Sla het bestand op en sluit het.

Open vervolgens App.js:

nano src/components/App/App.js

Verwijder de getToken en setToken functies uit App.js, en verwijder de import van useState. Importeer en gebruik in plaats daarvan de useToken Hook:

import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
import useToken from './useToken';

function App() {

  const { token, setToken } = useToken();

  if(!token) {
    return <Login setToken={setToken} />
  }

  return (
    <div className="wrapper">
      <h1>Application</h1>
      <BrowserRouter>
        <Routes>
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/preferences" element={<Preferences />} />
        </Routes>
      </BrowserRouter>
    </div>
  );
}

export default App;

Sla het bestand op en sluit het. De browser wordt automatisch vernieuwd. Na het inloggen navigeert de app direct naar de beveiligde pagina omdat useState in de aangepaste Hook een her-render triggert wanneer de tokenstatus wordt bijgewerkt:

De custom Hook slaat tokens op in sessionStorage en beheert de React-state. Het vernieuwen van de pagina houdt de gebruiker ingelogd omdat het token in sessionStorage blijft bestaan. Het openen van de applicatie in een nieuw tabblad logt de gebruiker echter uit, omdat sessionStorage tabblad-specifiek is, gegevens niet tussen tabbladen worden gedeeld en worden verwijderd wanneer het tabblad wordt gesloten.

Wanneer sessionStorage te gebruiken: Gebruik sessionStorage wanneer je sessie-specifieke authenticatie wilt die verdwijnt zodra het tabblad wordt gesloten. Dit is veiliger voor gedeelde computers.

Wanneer localStorage te gebruiken: Gebruik localStorage wanneer je wilt dat de authenticatie behouden blijft over tabbladen en browsersessies heen. Dit zorgt voor een betere gebruikerservaring, maar vereist dat gebruikers expliciet uitloggen op gedeelde computers.

Gebruik van localStorage om gegevens tussen vensters op te slaan

localStorage blijft behouden tussen browser-sessies en tabbladen, in tegenstelling tot sessionStorage, dat specifiek is voor één tabblad. Dit betekent dat gebruikers ingelogd blijven bij het openen van nieuwe tabbladen of het opnieuw starten van de browser.

Beveiligingsoverweging: Op gedeelde computers blijven localStorage-tokens behouden, zelfs na het sluiten van de browser. De volgende gebruiker kan toegang krijgen tot de applicatie zonder in te loggen als de vorige gebruiker niet heeft uitgelogd. Voor gedeelde computers, overweeg het volgende:

  • Waarschuwingen voor sessieverlooptijd toevoegen
  • Het implementeren van 'Onthoud mij' als een opt-in functie
  • Standaard sessionStorage gebruiken en localStorage alleen wanneer dit expliciet wordt gevraagd
  • Het bieden van duidelijke uitlogfunctionaliteit

Voor persoonlijke apparaten biedt localStorage een betere gebruikerservaring met een minimaal beveiligingsrisico wanneer het wordt gecombineerd met adequate XSS-bescherming.

Om naar localStorage te converteren, open useToken.js:

nano src/components/App/useToken.js

Vervang alle sessionStorage-verwijzingen door localStorage. De API-methoden (getItem, setItem) zijn identiek:

import { useState } from 'react';

export default function useToken() {
  const getToken = () => {
    const tokenString = localStorage.getItem('token');
    if (!tokenString) {
      return null;
    }
    const userToken = JSON.parse(tokenString);
    return userToken?.token;
  };

  const [token, setToken] = useState(getToken());

  const saveToken = userToken => {
    localStorage.setItem('token', JSON.stringify(userToken));
    setToken(userToken.token);
  };

  return {
    setToken: saveToken,
    token
  }
}

Sla het bestand op. De browser wordt automatisch vernieuwd. Log opnieuw in (het vorige token stond in sessionStorage, niet in localStorage). Na het inloggen blijft het token in localStorage, zodat je ingelogd blijft bij het openen van nieuwe tabbladen of het herstarten van de browser.

Je hebt persistente tokenopslag geïmplementeerd met sessionStorage en localStorage, en een aangepaste useToken Hook gemaakt die opslagbewerkingen combineert met React-statebeheer. De Hook veroorzaakt opnieuw rendering wanneer tokens veranderen, waardoor de UI correct wordt bijgewerkt. Je hebt ook de afwegingen tussen sessionStorage (specifiek voor tabbladen, veiliger) en localStorage (blijft behouden tussen sessies, betere UX) geleerd.

Stap 4 — Het implementeren van beveiligde routes met React Router v6

Beveiligde routes vereisen authenticatie voordat ze worden weergegeven. In React Router v6 maak je een wrappercomponent die controleert op een token en niet-geauthenticeerde gebruikers doorstuurt naar de loginpagina, terwijl hun beoogde bestemming behouden blijft.

Waarom een ProtectedRoute-component gebruiken: In plaats van authenticatie in elke component te controleren of conditioneel renderen in App.js te gebruiken, centraliseert een speciale ProtectedRoute-component de logica voor routebeveiliging. Dit maakt het eenvoudiger om beveiligde routes toe te voegen, omleidingen af te handelen en consistent gedrag voor authenticatie te behouden.

Maak een nieuw bestand voor de ProtectedRoute-component:

nano src/components/ProtectedRoute/ProtectedRoute.js

Binnen ProtectedRoute.js, importeer Navigate van react-router-dom en maak een component die controleert op een token:

import { Navigate } from 'react-router-dom';

export default function ProtectedRoute({ children, token }) {
  if (!token) {
    return <Navigate to="/login" replace />;
  }

  return children;
}

Het ProtectedRoute-component neemt children (het te beschermen routecomponent) en token als props. Als er geen token bestaat, wordt doorgestuurd naar /login met behulp van Navigate en de replace-prop om te voorkomen dat er een nieuwe vermelding in de browsergeschiedenis wordt toegevoegd.

Werk nu App.js bij om ProtectedRoute te gebruiken:

import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
import ProtectedRoute from '../ProtectedRoute/ProtectedRoute';
import useToken from './useToken';

function App() {
  const { token, setToken } = useToken();

  return (
    <div className="wrapper">
      <h1>Application</h1>
      <BrowserRouter>
        <Routes>
          <Route path="/login" element={<Login setToken={setToken} />} />
          <Route
            path="/dashboard"
            element={
              <ProtectedRoute token={token}>
                <Dashboard />
              </ProtectedRoute>
            }
          />
          <Route
            path="/preferences"
            element={
              <ProtectedRoute token={token}>
                <Preferences />
              </ProtectedRoute>
            }
          />
        </Routes>
      </BrowserRouter>
    </div>
  );
}

export default App;

Met deze aanpak worden niet-geauthenticeerde gebruikers die /dashboard of /preferences bezoeken, doorgestuurd naar /login.

Opmerking: In React Router v6 kun je ook de Outlet component gebruiken voor geneste routes en routebescherming. Voor eenvoudigere applicaties is de hier getoonde ProtectedRoute wrapper-component voldoende en gemakkelijker te begrijpen.

Stap 5 — JWT-tokens decoderen en vervaldatum controleren

JWT-tokens bevatten vervalinformatie in hun payload. Je kunt de token aan de clientzijde decoderen om te controleren of deze is verlopen zonder een verzoek naar de server te sturen, wat de gebruikerservaring verbetert door direct feedback te geven.

Waarom tokens aan de clientzijde decoderen: Hoewel je tokens altijd op de server moet valideren, stelt decodering aan de clientzijde je in staat om de vervaldatum te controleren voordat je API-aanroepen doet, passende foutmeldingen te tonen en gebruikers met verlopen tokens automatisch uit te loggen.

Installeer eerst een JWT-decoderingsbibliotheek. Hoewel je JWT's handmatig kunt decoderen (ze zijn base64-gecodeerd), zorgt het gebruik van een bibliotheek voor het afhandelen van randgevallen en biedt het betere foutafhandeling:

npm install jwt-decode

Werk je useToken.js-hook bij om tokens te decoderen en te valideren:

import { useState } from 'react';
import { jwtDecode } from 'jwt-decode';

export default function useToken() {
  const getToken = () => {
    const tokenString = localStorage.getItem('token');
    if (!tokenString) {
      return null;
    }
    
    const userToken = JSON.parse(tokenString);
    const token = userToken?.token;
    
    if (!token) {
      return null;
    }

    // Decode the token to check expiration
    try {
      const decoded = jwtDecode(token);
      const currentTime = Date.now() / 1000; // Convert to seconds
      
      // If token is expired, remove it and return null
      if (decoded.exp < currentTime) {
        localStorage.removeItem('token');
        return null;
      }
      
      return token;
    } catch (error) {
      // If decoding fails, token is invalid
      localStorage.removeItem('token');
      return null;
    }
  };

  const [token, setToken] = useState(getToken());

  const saveToken = userToken => {
    localStorage.setItem('token', JSON.stringify(userToken));
    setToken(userToken.token);
  };

  const removeToken = () => {
    localStorage.removeItem('token');
    setToken(null);
  };

  return {
    setToken: saveToken,
    token,
    removeToken
  };
}

De getToken-functie decodeert nu de JWT en controleert de exp-claim (vervaldatum in seconden). Als het token verlopen of ongeldig is, verwijdert het het uit de opslag en retourneert null, waarmee de gebruiker effectief wordt uitgelogd.

Waarschuwing: Token-decoding aan de klantzijde is alleen voor de gebruikerservaring. Valideer altijd tokens op uw server. Kwaadwillende gebruikers kunnen code aan de klantzijde aanpassen of nep-tokens verzenden. Vertrouw nooit op validatie aan de klantzijde voor beveiligingsbeslissingen.

Stap 6 — Uitloggen en foutafhandeling implementeren

Uitloggen verwijdert het token uit de opslag en leidt naar de inlogpagina. Foutenafhandeling voorkomt dat de app crasht wanneer API-aanroepen mislukken en geeft feedback aan de gebruiker.

Waarom uitloggen implementeren: Zonder een uitlogfunctie moeten gebruikers handmatig de browseropslag leegmaken of wachten tot de tokens verlopen. Een uitlogknop biedt een duidelijke manier om sessies te beëindigen en is essentieel voor gedeelde computers.

Werk je Dashboard-component bij om een uitlogknop op te nemen:

import React from 'react';
import { useNavigate } from 'react-router-dom';

export default function Dashboard({ removeToken }) {
  const navigate = useNavigate();

  const handleLogout = () => {
    removeToken();
    navigate('/login');
  };

  return (
    <div>
      <h2>Dashboard</h2>
      <button onClick={handleLogout}>Logout</button>
    </div>
  );
}

Werk App.js bij om removeToken door te geven aan beveiligde routes:

import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
import ProtectedRoute from '../ProtectedRoute/ProtectedRoute';
import useToken from './useToken';

function App() {
  const { token, setToken, removeToken } = useToken();

  return (
    <div className="wrapper">
      <h1>Application</h1>
      <BrowserRouter>
        <Routes>
          <Route path="/login" element={<Login setToken={setToken} />} />
          <Route
            path="/dashboard"
            element={
              <ProtectedRoute token={token}>
                <Dashboard removeToken={removeToken} />
              </ProtectedRoute>
            }
          />
          <Route
            path="/preferences"
            element={
              <ProtectedRoute token={token}>
                <Preferences />
              </ProtectedRoute>
            }
          />
        </Routes>
      </BrowserRouter>
    </div>
  );
}

export default App;

Voeg nu foutafhandeling toe aan de logincomponent:

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import './Login.css';

async function loginUser(credentials) {
  const response = await fetch('http://localhost:8080/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(credentials)
  });

  if (!response.ok) {
    throw new Error(`Login failed: ${response.statusText}`);
  }

  const data = await response.json();
  return data;
}

export default function Login({ setToken }) {
  const [username, setUserName] = useState();
  const [password, setPassword] = useState();
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  const handleSubmit = async e => {
    e.preventDefault();
    setError(null);
    setLoading(true);

    try {
      const token = await loginUser({
        username,
        password
      });
      setToken(token);
    } catch (err) {
      setError(err.message || 'An error occurred during login');
    } finally {
      setLoading(false);
    }
  };

  return(
    <div className="login-wrapper">
      <h1>Please Log In</h1>
      {error && <div className="error-message">{error}</div>}
      <form onSubmit={handleSubmit}>
        <label>
          <p>Username</p>
          <input 
            type="text" 
            onChange={e => setUserName(e.target.value)}
            disabled={loading}
          />
        </label>
        <label>
          <p>Password</p>
          <input 
            type="password" 
            onChange={e => setPassword(e.target.value)}
            disabled={loading}
          />
        </label>
        <div>
          <button type="submit" disabled={loading}>
            {loading ? 'Logging in...' : 'Submit'}
          </button>
        </div>
      </form>
    </div>
  );
}

Login.propTypes = {
  setToken: PropTypes.func.isRequired
};

Het logincomponent behandelt nu fouten, toont laadstatussen en geeft feedback aan de gebruiker. Foutenafhandeling voorkomt dat de app crasht wanneer API-aanroepen mislukken en geeft de gebruiker feedback over wat er mis is gegaan.

Stap 7 — React Context gebruiken voor authenticatie

React Context biedt een manier om de authenticatiestatus te delen tussen componenten zonder props door te geven. Voor grotere applicaties is Context schaalbaarder dan het doorgeven van props door meerdere componentlagen.

Waarom Context gebruiken voor authenticatie: Wanneer de authenticatiestatus in veel componenten nodig is (navigatiebalken, gebruikersmenu's, beschermde routes), wordt het doorgeven van props omslachtig. Context biedt een nette manier om overal in de componentenhiërarchie toegang te krijgen tot de auth-status.

Maak een authenticatiecontext aan:

import React, { createContext, useContext, useState } from 'react';
import useToken from '../components/App/useToken';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const { token, setToken, removeToken } = useToken();
  const [user, setUser] = useState(null);

  const login = async (credentials) => {
    // Login logic here
    const response = await fetch('http://localhost:8080/login', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(credentials)
    });

    if (!response.ok) {
      throw new Error('Login failed');
    }

    const data = await response.json();
    setToken(data);
    // Optionally decode token to set user info
    return data;
  };

  const logout = () => {
    removeToken();
    setUser(null);
  };

  const value = {
    token,
    user,
    login,
    logout,
    isAuthenticated: !!token
  };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
}

Wikkel je app in met de AuthProvider:

import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { AuthProvider } from '../../contexts/AuthContext';
import './App.css';
import Dashboard from '../Dashboard/Dashboard';
import Login from '../Login/Login';
import Preferences from '../Preferences/Preferences';
import ProtectedRoute from '../ProtectedRoute/ProtectedRoute';

function App() {
  return (
    <AuthProvider>
      <div className="wrapper">
        <h1>Application</h1>
        <BrowserRouter>
          <Routes>
            <Route path="/login" element={<Login />} />
            <Route
              path="/dashboard"
              element={
                <ProtectedRoute>
                  <Dashboard />
                </ProtectedRoute>
              }
            />
            <Route
              path="/preferences"
              element={
                <ProtectedRoute>
                  <Preferences />
                </ProtectedRoute>
              }
            />
          </Routes>
        </BrowserRouter>
      </div>
    </AuthProvider>
  );
}

export default App;

Werk ProtectedRoute bij om de context te gebruiken:

import { Navigate } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';

export default function ProtectedRoute({ children }) {
  const { isAuthenticated } = useAuth();

  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }

  return children;
}

Nu kan elk component de authenticatiestatus gebruiken met useAuth(), waardoor het niet meer nodig is om props door meerdere lagen door te geven.

Stap 8 — Begrijpen van CSRF-bescherming voor cookie-gebaseerde authenticatie

CSRF (Cross-Site Request Forgery) aanvallen maken gebruik van het feit dat browsers automatisch cookies meesturen met verzoeken. Bij het gebruik van cookie-gebaseerde authenticatie moet je CSRF-bescherming implementeren om te voorkomen dat kwaadaardige sites namens gebruikers geauthenticeerde verzoeken doen.

Waarom CSRF-bescherming belangrijk is: Als je authenticatie afhankelijk is van cookies (in plaats van tokens in localStorage), kan een kwaadaardige website gebruikers misleiden om geauthenticeerde verzoeken naar je API te doen. Bijvoorbeeld, een aanvaller kan een formulier insluiten dat naar je API wordt verzonden, en als de gebruiker is ingelogd, voegt de browser automatisch het authenticatiecookie toe.

CSRF-beschermingsstrategieën:

  1. CSRF-tokens: Genereer een unieke token voor elke sessie en vereis deze in verzoeken. De token moet overeenkomen met degene die op de server is opgeslagen.

  2. SameSite Cookie-attribuut: Stel cookies in met SameSite=Strict of SameSite=Lax om te voorkomen dat ze worden verzonden bij cross-site verzoeken.

  3. Controle van de Origin-header: Controleer of de Origin- of Referer-header overeenkomt met uw domein.

  4. Dubbel Verzenden Cookie: Sla de CSRF-token op in zowel een cookie als een formulier veld, waarbij beide overeen moeten komen.

Hier is een voorbeeld van het implementeren van CSRF-bescherming in je Express-server:

const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');

const app = express();

app.use(cors({
  origin: 'http://localhost:3000',
  credentials: true // Allow cookies
}));
app.use(cookieParser());
app.use(express.json());

// Generate CSRF token
app.get('/api/csrf-token', (req, res) => {
  const token = crypto.randomBytes(32).toString('hex');
  res.cookie('csrf-token', token, {
    httpOnly: false, // Must be accessible to JavaScript
    sameSite: 'strict',
    secure: process.env.NODE_ENV === 'production'
  });
  res.json({ csrfToken: token });
});

// Verify CSRF token middleware
function verifyCsrfToken(req, res, next) {
  const token = req.headers['x-csrf-token'];
  const cookieToken = req.cookies['csrf-token'];

  if (!token || !cookieToken || token !== cookieToken) {
    return res.status(403).json({ error: 'Invalid CSRF token' });
  }

  next();
}

app.post('/login', verifyCsrfToken, (req, res) => {
  // Login logic here
  res.json({ token: 'test123' });
});

app.listen(8080, () => console.log('API is running on http://localhost:8080'));

Haal in je React-app het CSRF-token op voordat je geauthenticeerde verzoeken doet:

let csrfToken = null;

export async function getCsrfToken() {
  if (!csrfToken) {
    const response = await fetch('http://localhost:8080/api/csrf-token', {
      credentials: 'include' // Include cookies
    });
    const data = await response.json();
    csrfToken = data.csrfToken;
  }
  return csrfToken;
}

export async function loginUser(credentials) {
  const token = await getCsrfToken();
  
  return fetch('http://localhost:8080/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': token
    },
    credentials: 'include', // Include cookies
    body: JSON.stringify(credentials)
  }).then(data => data.json());
}

Opmerking: CSRF-bescherming is niet nodig voor op tokens gebaseerde authenticatie die in localStorage wordt opgeslagen, omdat tokens expliciet in de requestheaders moeten worden meegegeven en niet automatisch met requests worden verzonden. Als je echter cookies gebruikt voor authenticatie, is CSRF-bescherming essentieel, omdat cookies automatisch worden verzonden. Ongeacht je authenticatiemethode, implementeer altijd de juiste beveiligingsmaatregelen zoals HTTPS, invoervalidatie en rate limiting.

Probleemoplossing

Token blijft niet na het vernieuwen van de pagina

Probleem: Na het inloggen zorgt het vernieuwen van de pagina ervoor dat je wordt uitgelogd.

Oplossingen:

  • Controleer of tokens zijn opgeslagen in localStorage of sessionStorage (controleer browser DevTools > Application > Storage)
  • Zorg ervoor dat getToken() wordt uitgevoerd bij het laden van de component en de status correct initialiseert
  • Controleer de browserconsole op fouten die het ophalen van tokens kunnen verhinderen
  • Verifieer of het tokenformaat overeenkomt met wat je code verwacht (object met token-eigenschap)

„Kan de eigenschap ‘token’ van null niet lezen” Fout

Probleem: Fout treedt op bij het openen van userToken.token na JSON.parse().

Oplossing: Voeg een null-controle toe voordat je gaat parsen:

const tokenString = localStorage.getItem('token');
if (!tokenString) return null;
const userToken = JSON.parse(tokenString);
return userToken?.token;

Inlogformulier wordt niet verzonden

Probleem: Op de knop verzenden klikken activeert de login-aanvraag niet.

Oplossingen:

  • Zorg ervoor dat het formulier onSubmit={handleSubmit} heeft en dat de knop type="submit" heeft
  • Controleer of de API-server draait op de juiste poort (standaard: 8080)
  • Controleer of CORS is ingeschakeld op de server als er cross-origin verzoeken worden gedaan
  • Controleer de browserconsole op netwerkfouten of CORS-problemen

Beschermde routes werken niet

Probleem: Gebruikers kunnen toegang krijgen tot beveiligde routes zonder authenticatie.

Oplossingen:

  • Controleer of de ProtectedRoute-component het token controleert voordat deze wordt weergegeven
  • Zorg ervoor dat het token wordt doorgegeven aan ProtectedRoute of via Context wordt benaderd
  • Controleer of de routes correct zijn ingepakt in App.js
  • Controleer de React Router v6-syntaxis (element prop, niet children)

Token verloopt terwijl gebruiker actief is

Probleem: Gebruiker wordt onverwacht uitgelogd tijdens actief gebruik.

Oplossingen:

  • Implementeer logica voor het vernieuwen van tokens met behulp van refresh-tokens
  • Controleer de tokenvervaldatum voordat je API-aanroepen doet
  • Automatisch token vernieuwen toevoegen wanneer de vervaldatum nadert
  • Overweeg de tokenvervaldatum te verlengen voor actieve gebruikers

localStorage uitgeschakeld of limiet overschreden

Probleem: Tokens worden niet opgeslagen in privémodus/incognitomodus of de opslagruimte is vol.

Oplossingen:

  • Controleer of localStorage beschikbaar is: if (typeof(Storage) !== "undefined")
  • Omgaan met QuotaExceededError bij het instellen van items
  • Val terug op sessionStorage of opslag in het geheugen
  • Wis oude tokens voordat je nieuwe opslaat
  • Informeer gebruikers als opslag niet beschikbaar is

Meerdere tabbladen synchroniseren de authenticatiestatus niet

Probleem: Uitloggen in één tabblad logt niet uit in andere tabbladen.

Oplossing: Luister naar storage-evenementen om de status tussen tabbladen te synchroniseren:

useEffect(() => {
  const handleStorageChange = (e) => {
    if (e.key === 'token') {
      // Update auth state when token changes in another tab
      setToken(e.newValue ? JSON.parse(e.newValue) : null);
    }
  };
  window.addEventListener('storage', handleStorageChange);
  return () => window.removeEventListener('storage', handleStorageChange);
}, []);

API retourneert een ander tokenformaat

Probleem: Uw API retourneert { accessToken: ''...'' } in plaats van { token: ''...'' }.

Oplossing: Werk de code bij zodat deze overeenkomt met het antwoordformaat van je API:

// If API returns { accessToken: '...' }
const token = userToken?.accessToken;

// Or if API returns { data: { token: '...' } }
const token = userToken?.data?.token;

401 Niet Geautoriseerd Fouten

Probleem: API geeft 401 terug, zelfs met een geldig token.

Oplossingen:

  • Controleer of het token wordt verzonden in de request-headers: Authorization: Bearer $ {token}
  • Controleer tokenvervaldatum (decodeer JWT om de exp-claim te zien)
  • Zorg ervoor dat het tokenformaat overeenkomt met wat de server verwacht
  • Controleer of de server tokens correct valideert
  • Controleer of het token vernieuwd moet worden

Veelgestelde vragen

1. Hoe bescherm ik routes in React?

Gebruik een ProtectedRoute wrapper-component die de authenticatie controleert voordat deze wordt weergegeven. In React Router v6, wikkel beschermde routes in met een component die verifieert dat het token aanwezig is. Als er geen token wordt gevonden, word je doorgestuurd naar de inlogpagina met Navigate:

function ProtectedRoute({ children, token }) {
  if (!token) {
    return <Navigate to="/login" replace />;
  }
  return children;
}

Gebruik het dan in je routes:

<Route
  path="/dashboard"
  element={
    <ProtectedRoute token={token}>
      <Dashboard />
    </ProtectedRoute>
  }
/>

2. Waar moet ik authenticatietokens opslaan?

Sla tokens op in localStorage voor persistentie tussen sessies, of sessionStorage voor sessies die specifiek zijn voor een tabblad. Beiden zijn toegankelijk voor JavaScript, waardoor ze kwetsbaar zijn voor XSS-aanvallen, dus filter altijd gebruikersinvoer en gebruik HTTPS.

  • localStorage: Blijft behouden tussen browsersessies en -tabs. Gebruik dit voor de 'onthoud mij'-functionaliteit.
  • sessionStorage: Wordt gewist wanneer het tabblad wordt gesloten. Gebruik voor veiligere, alleen-sessie authenticatie.
  • Cookies: Automatisch meegestuurd met verzoeken, wat CSRF-bescherming vereist. Gebruik met de httpOnly-vlag voor alleen server-toegang.

Voor productieapplicaties wordt aanbevolen om veilige, httpOnly-cookies te gebruiken die door je backend worden beheerd, omdat deze minder kwetsbaar zijn voor XSS-aanvallen.

3. Is localStorage veilig voor authenticatie?

localStorage is kwetsbaar voor XSS-aanvallen maar is acceptabel voor veel toepassingen wanneer het wordt gecombineerd met de juiste beveiligingsmaatregelen. Elke JavaScript die op jouw domein draait, kan localStorage benaderen, dus als een aanvaller kwaadaardige scripts injecteert, kunnen ze tokens stelen.

Mitigatiestrategieën:

  • Reinig alle gebruikersinvoer om XSS te voorkomen
  • Gebruik Content Security Policy (CSP)-headers
  • Implementeer tokenverval en vernieuw tokens
  • Gebruik HTTPS om man-in-the-middle-aanvallen te voorkomen
  • Overweeg httpOnly-cookies voor gevoelige toepassingen

Voor toepassingen met hoge beveiliging (bankieren, gezondheidszorg) heeft u de voorkeur aan httpOnly-cookies met CSRF-bescherming, omdat ze niet toegankelijk zijn voor JavaScript.

4. Hebben React-apps OAuth nodig?

OAuth is vereist bij het authenticeren met externe providers (Google, GitHub, Facebook), maar niet voor aangepaste authenticatie. Als je je eigen authenticatiesysteem met gebruikersnaam/wachtwoord bouwt, kun je token-gebaseerde authenticatie gebruiken zoals getoond in deze handleiding.

Gebruik OAuth wanneer:

  • Gebruikers moeten inloggen met sociale accounts
  • U wilt de authenticatie delegeren aan een vertrouwde leverancier
  • Je moet namens gebruikers toegang krijgen tot API's van derden

Gebruik aangepaste authenticatie wanneer:

  • Je beheert de gebruikersdatabase
  • Je hebt volledige controle over de authenticatiestroom nodig
  • Je wilt afhankelijkheden van derden vermijden

Veel toepassingen gebruiken een hybride benadering: OAuth voor sociale login en aangepaste authenticatie voor e-mail/wachtwoord.

5. Hoe ga ik om met het verlopen van een token?

Decodeer de JWT-token aan de clientzijde om de vervaldatum te controleren, en ververs of log de gebruiker vervolgens uit. Gebruik een bibliotheek zoals jwt-decode om de exp-claim te lezen:

import { jwtDecode } from 'jwt-decode';

const decoded = jwtDecode(token);
const currentTime = Date.now() / 1000;

if (decoded.exp < currentTime) {
  // Token expired, remove it and redirect to login
  localStorage.removeItem('token');
  navigate('/login');
}

Voor productie, implementeer refresh-tokens: kortdurende toegangstokens (15-30 minuten) en langer geldende refresh-tokens (7-30 dagen) die kunnen worden gebruikt om nieuwe toegangstokens te verkrijgen zonder opnieuw te hoeven authenticeren.

6. Hoe implementeer ik de 'Onthoud mij'-functionaliteit?

Sla tokens op in localStorage voor persistente sessies en in sessionStorage voor tijdelijke sessies. Voeg een selectievakje toe aan je inlogformulier:

const [rememberMe, setRememberMe] = useState(false);

const handleSubmit = async (e) => {
  e.preventDefault();
  const token = await loginUser(credentials);
  
  if (rememberMe) {
    localStorage.setItem('token', JSON.stringify(token));
  } else {
    sessionStorage.setItem('token', JSON.stringify(token));
  }
};

Wanneer rememberMe is aangevinkt, blijft het token behouden tussen browsersessies. Anders wordt het verwijderd wanneer het tabblad wordt gesloten.

Conclusie

Authenticatie in React-toepassingen vereist het vinden van een balans tussen beveiliging, gebruikerservaring en onderhoudbaarheid. Door authenticatielogica te centraliseren in aangepaste hooks of React Context, beveiligde routes te implementeren met React Router v6 en geschikte strategieën voor tokenopslag te kiezen, kun je veilige en gebruiksvriendelijke authenticatiestromen bouwen.

Belangrijke principes om te onthouden:

  • Sla tokens op in browseropslag (localStorage of sessionStorage) om ze te behouden bij het verversen van de pagina
  • Gebruik beschermde routecomponenten om authenticatievereisten af te dwingen
  • Decodeer JWT-tokens aan de clientkant om de vervaldatum te controleren, maar valideer ze altijd op de server.
  • Implementeer foutafhandeling en laadstatussen voor een betere gebruikservaring
  • Houd rekening met CSRF-bescherming bij het gebruik van cookie-gebaseerde authenticatie
  • Gebruik React Context voor de authenticatiestatus in grotere applicaties om prop drilling te vermijden

Elke opslagoplossing en authenticatieaanpak heeft afwegingen. localStorage biedt gemak, maar vereist bescherming tegen XSS. sessionStorage is veiliger maar minder handig. Cookies vereisen bescherming tegen CSRF, maar zijn minder kwetsbaar voor XSS. Kies op basis van je beveiligingsvereisten en doelen voor de gebruikerservaring.

Naarmate je applicatie groeit, overweeg dan het implementeren van refresh-tokens, multi-factor authenticatie en sessiebeheerfuncties. Voor productieapplicaties, gebruik altijd HTTPS, implementeer rate limiting en volg de beste beveiligingspraktijken.

Als je meer React-tutorials wilt lezen, bekijk dan onze React-onderwerppagina, of ontdek gerelateerde tutorials zoals Hoe je een To-Do-applicatie bouwt met Django en React en Hoe je een React-applicatie implementeert met Nginx op Ubuntu.

Bedankt dat je samen met de Linux-Console.net-community hebt geleerd.