Ricerca nel sito web

Crea percorsi dinamici in Next.js con Route Guards e autenticazione utente


introduzione

Quando si creano siti Web scalabili, il compito più importante di uno sviluppatore è la gestione dei dati dinamici. Per un sito Web che vende migliaia di prodotti, sarà quasi impossibile per uno sviluppatore creare percorsi tradizionali/statici per tutti quei prodotti. È qui che il routing dinamico torna utile. I percorsi dinamici utilizzano un layout di pagina Web standard per visualizzare il contenuto mentre il contenuto stesso viene recuperato dal server in base ai dati dinamici richiesti dal client.

Attraverso questo tutorial imparerai come implementare percorsi dinamici in un'app Next.js. Next.js è un eccellente framework basato su React che gestisce sia le azioni lato client che quelle lato server. Imparerai come utilizzare il routing basato su file di Next.js per implementare percorsi dinamici e come mettere in atto protezioni del percorso per proteggere il tuo server.

Nota: Per gli utenti che hanno utilizzato le versioni di Next.js <13.2, questa sarà una nuova sintassi perché Next.js è passato a una nuova architettura.

Prerequisiti

Per seguire questo tutorial avrai bisogno di:

  • Gestore di pacchetti Node.js e npm (o filato). Puoi fare riferimento a questo tutorial di DigitalOcean su come installare le ultime versioni di Node.js e npm sul tuo computer.
  • Un computer Windows, MacOS o Linux con accesso a Internet.
  • Fondamenti di React e NodeJS.
  • Comunità MongoDB o versione Atlas.

In questo tutorial imparerai come implementare percorsi dinamici in un'applicazione Next.js. Imparerai prima a creare un progetto Next.js, quindi come implementare il routing dinamico da zero. Successivamente, gestirai i percorsi che non hai impostato (gestione dell'errore 404). Nelle sezioni successive imparerai come impostare l'autenticazione per creare protezioni del percorso per impedire agli utenti di accedere a percorsi per i quali non sono autorizzati.

Passaggio 2: creazione di percorsi dinamici in Next.js

Apri un terminale e digita il comando seguente per creare un progetto Next.js utilizzando create-next-app.

npx create-next-app@latest

Una volta avviato il processo, ti verranno chiesti alcuni input e preferenze:

Need to install the following packages:
create-next-app@14.1.3
Ok to proceed? (y) y
√ What is your project named? ... sample_app
√ Would you like to use TypeScript? ... No / Yes
√ Would you like to use ESLint? ... No / Yes
√ Would you like to use Tailwind CSS? ... No / Yes
√ Would you like to use `src/` directory? ... No / Yes
√ Would you like to use App Router? (recommended) ... No / Yes
√ Would you like to customize the default import alias (@/*)? ... No / Yes

Ora puoi selezionare se desideri TypeScript, ESLint o Tailwind nel tuo progetto. Tuttavia, per continuare con questo tutorial devi selezionare Sì su App Router. Inoltre, puoi scegliere di mantenere i file sorgente in src o modificare l'alias di importazione predefinito (che è @/).

Ora che hai impostato il progetto, devi comprendere l'architettura Next.js. L'ultima versione di Next.js (ovvero la versione 14.x) utilizza una struttura di progetto diversa rispetto alle versioni precedenti.

Ottieni una directory app che contiene tutti i file sorgente. Quindi, all'interno della directory dell'app, c'è page.js che è l'equivalente Next.js di App.js in un'applicazione React. page.js è il punto di ingresso per ogni percorso e sottopercorso.

Come funziona il routing in Next.js?

Next.js utilizza il routing basato su file. Nella versione corrente di Next.js, la directory dell'app corrisponde al percorso /. Il page.js nella directory dell'app controlla il comportamento della pagina /. Per creare percorsi secondari, devi creare sottodirectory nella directory dell'app e aggiungere page.js a ciascun endpoint.

Ad esempio, devi creare due percorsi: tuo_dominio/utente e tuo_dominio/blog/post.

Per creare il primo percorso, devi creare una sottodirectory nella directory dell'app denominata user e inizializzare un page.js in quella sottodirectory.

--app
|--user
|--|--page.js

In questo page.js creerai la logica per il tuo endpoint utente. Allo stesso modo, per creare il secondo percorso, crea directory nidificate come le seguenti.

--app
|--blog
|--|--page.js
|--|--post
|--|--|--page.js

Nota: Puoi saltare page.js nella directory app/blog se non ti serve quell'endpoint. In questo caso, Next.js eseguirà il rendering di una pagina 404.

In questo modo, puoi creare percorsi statici nidificati all'infinito in Next.js. Tuttavia, la creazione di percorsi dinamici richiede una sintassi diversa, che imparerai nella sezione successiva.

Creazione della directory per gestire percorsi dinamici

Supponiamo ora di dover creare un percorso dinamico semplice, nel percorso home dell'applicazione. Quindi, la sintassi per creare percorsi dinamici come tuo_dominio/:params è:

--app
|--page.js
|--[slug]
|--|--page.js

Qui hai creato una directory denominata slug racchiusa tra []. Questo dice a Next.js che questo percorso sarà dinamico. Usare lo slug del nome è solo una convenzione, puoi sostituirlo con qualsiasi nome; lo slug funge essenzialmente da segnaposto per i dati dinamici che verranno passati utilizzando un percorso dinamico.

Configurazione del routing basato su file per percorsi dinamici

Per testare la directory impostata nella sezione precedente, apri page.js nella tua directory [slug] e digita il seguente codice:

  export default function Page({params}){
  return (
    <div>My slug is: {params.slug}</div>
  )
}

In questo caso, l'argomento {params} consente all'endpoint di accedere ai parametri URL. Quindi, visualizziamo semplicemente il valore dello slug utilizzando params.slug (sostituisci lo slug con il segnaposto che hai utilizzato nel nome della directory). Ora esegui la tua applicazione utilizzando il seguente comando:

npm run dev

Una volta compilato, vai a http:localhost:3000/sammy Qui puoi sostituire sammy con qualsiasi dato alfanumerico e otterrai gli stessi dati all'interno della tua pagina come segue:

Puoi annidare percorsi dinamici simili ai percorsi statici, come spiegato nella sezione precedente. Inoltre, puoi creare un percorso nidificato contenente un numero qualsiasi di percorsi statici e dinamici al suo interno. Nella sezione successiva imparerai come impostare l'autenticazione utente per i tuoi percorsi dinamici.

Passaggio 3: creazione dell'autenticazione utente

La sezione precedente spiegava come creare percorsi dinamici utilizzando i dati dinamici passati utilizzando i parametri URL. Tuttavia, questo crea un problema per te come sviluppatore. Questo approccio consente di eseguire il rendering di infinite pagine possibili utilizzando parametri diversi nell'URL dell'applicazione, il che non è una buona idea. Pertanto, è necessario creare una logica che impedisca la creazione di percorsi non necessari e l'accesso non autorizzato a questi percorsi dinamici.

In questa sezione configurerai un sistema di autenticazione dell'utente, che a sua volta ti consentirà di gestire la sicurezza dei tuoi percorsi.

Installazione di dipendenze aggiuntive

Ora creerai un sistema di recupero dell'iscrizione degli utenti utilizzando il rendering lato server in Next.js. Per iniziare con le sezioni seguenti, devi installare le dipendenze Mongoose e JSON nel tuo progetto Next.js. È possibile installare questi pacchetti con il seguente comando:

cd <project directory>

npm i mongoose jsonwebtoken

Dopo averli installati, vai avanti con il tutorial.

Impostazione di percorsi API, connessione Mongoose e modelli

Ora creerai un'API nella tua applicazione Next.js. Per fare ciò, crea una directory all'interno della tua directory /app denominata api. Questa directory conterrà tutti i percorsi per la nostra API di backend, che utilizzeremo per aggiungere utenti e recuperare i dettagli di un utente dal database MongoDB.

La creazione di sottopercorsi nella route API è simile alla creazione di una route lato client. Creerai una directory con il nome del tuo percorso, all'interno di quella directory creerai un file route.js, che gestirà la logica per il tuo percorso API.

Per questo tutorial, dobbiamo creare i seguenti percorsi: /api/adduser e /api/getuser. È possibile definire questi percorsi creando le directory e i file seguenti.

app/
└── api/
    ├── adduser/
    │   └── route.js
    └── getuser/
        └── route.js

Sarebbe qualcosa di simile al seguente:

Dopo aver creato le directory per le rotte API, devi configurare la tua connessione Mongoose a MongoDB. Se non hai familiarità con Mongoose, fai riferimento a questo tutorial di DigitalOcean su Come eseguire operazioni CRUD con Mongoose e MongoDB Atlas | DigitalOcean.

Ora configureremo la nostra connessione e i file di schema allo stesso livello della directory dell'app, il che semplifica l'importazione in qualsiasi altro file. Crea una directory denominata db nella directory del tuo progetto root. All'interno di questa directory db, crea due file:

  1. connect.js – Per gestire le connessioni a MongoDB.

  2. UserModel.js – Per definire lo schema per il tuo profilo utente.

La struttura del file sarebbe simile alla seguente:

Ora apri il file connect.js e aggiungi il seguente codice:

import mongoose from "mongoose";

const connectDB = async () => {
  try {
    if (mongoose.connection.readyState === 0) {      // checking currently open connections
      await mongoose.connect("mongodb://127.0.0.1:27017/sammy");
      console.log("MongoDB Connected...");
    } else {
      console.log("Using existing MongoDB connection...");
    }
  } catch (error) {
    console.error("Error connecting to MongoDB:", error);
    process.exit(1);
  }
};

export default connectDB;

Informazioni: In questo tutorial, la versione community di MongoDB viene utilizzata con un database denominato sammy.

Qui stai importando il modulo Mongoose ed esportando una funzione connectDB, che crea una nuova connessione MongoDB se non esiste una connessione esistente. Se esiste un'altra connessione, questa utilizzerà la stessa connessione invece di crearne una nuova per ogni richiesta.

Avvertenza: non utilizzare localhost invece dell'indirizzo IP 127.0.0.1. MongoDB non lo supporta.

Ora creerai uno schema utente per gestire tutte le richieste effettuate al tuo database. Apri il file UserModel.js e digita il seguente codice.

import mongoose from "mongoose";

const userSchema = new mongoose.Schema({
  name: { type: String, required: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
});

const User = mongoose.model("User", userSchema);

mongoose.models = {}; // prevents rewriting of mongoose model
module.exports = User;

Qui crei uno schema denominato userSchema. Questo schema definisce la struttura dei tuoi documenti all'interno della raccolta User. Un modello Mongoose si traduce in una raccolta nel database MongoDB.

Nota: in Next.js, devi aggiungere la riga mongoose.models={} poiché ciò impedisce la sovrascrittura del modello dopo ogni richiesta. Se non aggiungi questa riga, la tua app genererà un errore su tutte le richieste tranne la prima.

Nella sezione successiva creerai la logica per i tuoi percorsi.

Creazione di percorsi API

Ora creeremo la logica per tutte le rotte API, iniziando con la rotta adduser.

/api/adduser Percorso

Apri route.js all'interno della directory adduser e aggiungi il seguente codice:

import connectDB from "@/db/connect";
import User from "@/db/UserModel";
const jwt = require("jsonwebtoken");
const secret = "YOUR_JWT_SECRET_KEY";

export async function POST(Request) {
  await connectDB();

  try {
    const data = await Request.json();

    let user = new User({
      name: data.name,
      email: data.email,
      password: data.password,
    });

    await user.save();

    const token = jwt.sign(user.id, secret);

return new Response("", {
        status: 200,
        headers: { Authorization: token },
      });
  } catch (error) {
    console.log(error);
  }
}

Qui importiamo la funzione connectDB e il modello User dalla nostra directory db. Quindi, importiamo il modulo jsonwebtoken per gestire la nostra logica di autenticazione. Se non hai mai utilizzato il pacchetto jsonwebtoken, fai riferimento a questa guida su Come utilizzare i token Web JSON (JWT) in Express.js per aiutarti a comprenderne l'utilizzo.

Ora creerai una richiesta POST nel gestore Route di NestJS utilizzando la funzione async/await. Tieni presente che la gestione del percorso nella versione Next.js 13.2+ è diversa rispetto alle versioni precedenti e in qualsiasi altro framework JavaScript.

Innanzitutto, chiami la funzione connectDB per stabilire una connessione a MongoDB. Quindi, recuperi i dati dal corpo della richiesta utilizzando Request.json(). Successivamente, creerai un nuovo oggetto User per creare un utente in base alle credenziali recuperate dal corpo della richiesta. Quindi, salva l'oggetto in MongoDB utilizzando la funzione save() di Mongoose.

Una volta creato l'account utente, crea un token JWT (jsonwebtoken) utilizzando la funzione segno. Questa funzione accetta un payload (user.id in questo caso) e una secret_key come parametro richiesto e restituisce un token con hash JWT. Puoi cambiare secret_key in qualsiasi stringa.

Informazioni:: si consiglia vivamente, per motivi di sicurezza, di archiviare la secret_key e qualsiasi altra credenziale in un file dell'ambiente di processo anziché codificarla nei propri file.

Infine, devi inviare una risposta JSON contenente il token come intestazione denominata Authentication per l'autenticazione su vari percorsi.

Avviso: non devi mai gestire le password come stringhe. In questo tutorial, le password vengono utilizzate come stringhe nel tentativo di semplificare i passaggi. Tuttavia, in un ambiente di produzione, è necessario gestire le password come hash.

Puoi utilizzare BcryptJS per gestire gli hash delle password nei framework JavaScript. Fare riferimento a questa guida su Come gestire le password in modo sicuro con BcryptsJS in JavaScript per una migliore comprensione dell'utilizzo di BcryptJS nei framework basati su Node.js.

/api/getuser Percorso

Ora creerai il secondo percorso necessario nella tua API. Il percorso getuser recupererà i dettagli dell'utente dal nostro database dopo l'autenticazione riuscita. Apri il file route.js nella directory getuser e aggiungi il seguente codice:

import connectDB from "@/db/connect";
import User from "@/db/UserModel";
import { headers } from "next/headers";
const jwt = require("jsonwebtoken");
const secret = "YOUR_JWT_SECRET_KEY";

export async function POST() {
  await connectDB();

  const headerList = headers();
  const token = headerList.get("Authorization");

  if (!token) {
    return Response.json({ success: false, msg: "Unauthorized!" });
  }

  try {
    const data = await jwt.verify(token, secret);
    let user = await User.findById(data).select("-password");

    if (!user) {
      return Response.json({ success: false, msg: "Unauthorized!" });
    }

    return Response.json({ success: true, user });

  } catch (error) {
    return Response.json({ success: false, msg: "Invalid token!" });
  }
}

Qui, effettui importazioni simili a quelle degli altri due percorsi, ad eccezione del modulo headers da next/headers. Ciò fornisce l'interfaccia per accedere alle intestazioni in un determinato percorso.

Ancora una volta, crei una richiesta POST e ti connetti a MongoDB. Quindi, recupererai tutte le intestazioni dalla richiesta HTTP utilizzando la funzione headers(). Recuperi l'intestazione Authentication da questo elenco di intestazioni e crei una risposta per la sua assenza.

Se esiste l'intestazione Authentication, prendiamo il token dal suo valore e lo verifichiamo utilizzando la funzione verify() del pacchetto jsonwebtoken (assicuratevi di fornire la stessa secret_key che hai utilizzato durante la creazione del token).

Una volta verificata con successo il token, otterrai il payload user.id come valore di ritorno della funzione verify(). Quindi utilizzerai questo user.id per eseguire una query di ricerca utilizzando la funzione findById() di Mongoose. Il select('-password') garantisce che tu ottenga tutti i dati nel documento utente tranne la password.

Se nel tuo database esiste un utente con l'ID recuperato dal payload del token, riceverai i suoi dettagli come risposta JSON con success: true. Altrimenti, riceverai una risposta success: false. Ciò garantisce che solo gli utenti esistenti con token autenticati possano accedere alle route nella tua applicazione.

Ora che hai completato l'API per implementare l'autenticazione dell'utente, utilizzerai questo sistema di autenticazione per applicare le protezioni del percorso sui percorsi dinamici creati sul tuo sito web.

Passaggio 4: aggiunta delle protezioni del percorso

Nella sezione precedente, hai imparato come creare un sistema di autenticazione di base nel backend Next.js utilizzando JWT e Mongoose. Ora utilizzerai il sistema di autenticazione backend per impedire l'accesso non autorizzato ai percorsi dinamici in un'app Next.js. Inizierai con la creazione di un percorso dinamico per la pagina del tuo profilo utente. Crea la seguente struttura di directory nella directory dell'app.

app/
└── user/
    ├── [slug]/
    │   └── page.js

Questa struttura creerà il seguente percorso dinamico per visualizzare i profili utente di diversi utenti.

your_domain/user/:slug

In questo comando, slug sarà il nome dell'utente. Ora, vuoi impedire che URL non necessari vengano consentiti nella tua applicazione lato client. Creerai la logica per questo nella sezione successiva.

Protezioni del percorso contro pagine inesistenti

Per impedire l'accesso a percorsi illegali, puoi inviare una risposta "404 Pagina non trovata" e reindirizzare qualsiasi percorso alla pagina 404. Next.js ha una propria pagina 404 personalizzata a cui puoi accedere con la funzione notFound() dalla libreria next/navigation.

Apri page.js all'interno di /user/[slug] e aggiungi il seguente codice al file:

"use client";
import { notFound } from "next/navigation";

const Page = ({ params })=> {

  // logic to handle allowed routes

  notFound();
}

export default  Page;

Questo sarà un componente client pertanto è necessario informare il compilatore Next.js che si tratta di un componente client. Questo può essere fatto aggiungendo una stringa all'inizio del file “use client ”. Questa sintassi informa il compilatore di trattare questo file come un componente client, non come server.

Successivamente, importi la pagina notFound() da next/navigation e crei una funzione Page. Qui, hai chiamato la funzione notFound() come unica risposta, quindi per impostazione predefinita ogni richiesta nella rotta /user/:slug reindirizzerà alla pagina 404 Itinerario non trovato.

Nella sezione successiva aggiungerai la logica per gestire i percorsi dinamici consentiti.

Informazioni: Next.js ti consente di progettare la tua pagina 404 personalizzata. Puoi ottenere questo risultato creando un file not-found.js nella directory dell'app e aggiungendovi il tuo codice personalizzato. Se il file not-found.js è presente nella directory dell'app, Next.js reindirizzerà tutte le richieste notFound() a quella pagina anziché alla pagina predefinita.

Protezioni del percorso che utilizzano l'autenticazione utente per l'accesso al percorso dinamico

Finora, hai configurato un'applicazione Next.js che crea utenti e, dopo la loro registrazione riuscita, mostra i dettagli dell'utente nella pagina del profilo. Inoltre, poiché stai utilizzando dati dinamici per visualizzare i profili utente, imposti una barriera virtuale che invia tutte le richieste non autorizzate al percorso della pagina del profilo e viene reindirizzata alla pagina 404.

Il metodo notFound() che hai aggiunto alla pagina del tuo profilo utente reindirizza tutte le richieste al percorso dinamico /user/:slug a Pagina non trovata. Ora aggiungerai la logica affinché un utente autenticato visualizzi i dettagli del suo profilo anziché una "Pagina 404 non trovata".

Innanzitutto, creerai un percorso Web per la registrazione dell'utente. Puoi farlo aggiungendo due directory nella directory dell'app.

app/
   ├── signup/
    │   └── page.js

Ora apri page.js nella directory app/signup e inserisci il seguente codice:

Si tratta di una porzione di codice di grandi dimensioni. Fare clic per visualizzare

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";

const Page = () => {
  const [email, setEmail] = useState("");
  const [name, setName] = useState("");
  const [password, setPassword] = useState("");
  const [error, setError] = useState(null);
  const router = useRouter();

  const handleSubmit = async (event) => {
    event.preventDefault();

    // Basic validation
    if (!email || !password || !name) {
      setError("Please fill in all fields...");
      return;
    }

    try {
      const response = await fetch("/api/adduser", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ email, name, password }),
      });

      if (!response.ok) {
        throw new Error("Sign Up failed");
      }

      const responseAuth = response.headers.get("Authorization");

      const userDetails = await fetch("/api/getuser", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: responseAuth,
        },
      });

      if (!userDetails.ok) {
        throw new Error("Sign Up failed");
      }
      const userData = await userDetails.json();

      localStorage.setItem(`userData`, JSON.stringify(userData.user));
      router.push(`/user/${userData.user.name}`);
    } catch (error) {
      console.log(error);
    }
  };

  return (
    <div>
      <h1>Sign Up</h1>
      {error && <p style={{ color: "red" }}>{error}</p>}
      <form onSubmit={handleSubmit}>
        <div>
          <label>Name:</label>
          <input
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
          />
          <label>Email:</label>
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>
        <div>
          <label>Password:</label>
          <input
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <button type="submit">Sign Up</button>
      </form>
    </div>
  );
};

export default Page;

In questa pagina hai creato un modulo che accetta l'input per il nome, l'e-mail e la password dell'utente. Successivamente, crei un gestore onSubmit per il modulo che chiama la funzione handleSubmit per gestire le richieste API con payload dal corpo.

Nella funzione handleSubmit(), esegui un semplice controllo per i valori di input. Quindi, esegui una chiamata API fetch a /api/adduser, che hai definito nelle sezioni precedenti (poiché sia il client che il server vengono eseguiti sulla stessa porta nell'applicazione Next.js, non è necessario fornire il percorso assoluto). Nel corpo della richiesta, invii il nome, l'email e la password dal corpo del modulo.

La rotta /api/adduser restituisce un'intestazione di autorizzazione. Memorizzi quell'intestazione in una variabile ed effettui una seconda chiamata API di recupero a /api/getuser inviando l'intestazione salvata dalla prima chiamata API come intestazione di autorizzazione.

Ciò restituirà i dettagli dell'utente (nome ed email). Memorizzi questi dettagli in localStorage come stringhe con la chiave userData e reindirizzi la pagina al percorso dinamico /user/:.

Ora apri page.js nel percorso dinamico /user/[slug] e modificalo nella seguente logica:

"use client";
import { notFound } from "next/navigation";

const Page = ({ params })=> {

  const userData = JSON.parse(localStorage.getItem("userData"));

  if (userData.name === params.slug) {
    return (
      <div>
        <p>User : {userData.name}</p>
        <p>Email: {userData.email}</p>
      </div>
    );
  }

  notFound();
}

export default  Page;

In questa logica aggiornata, recuperi userData da localStorage e controlli se lo slug corrisponde a user.name da userData. Se il controllo restituisce true, otterrai i dettagli dell'utente. In caso contrario, riceverai il messaggio 404 Pagina non trovata.

Informazioni: l'aggiunta di questa sicurezza aggiuntiva garantisce che, anche se sei un utente autorizzato, puoi accedere solo ai tuoi dati e non a quelli di qualcun altro.

La pagina di registrazione sarebbe simile a questa:

Vai avanti e riempi i campi con qualsiasi valore. Dopo aver fatto clic sul pulsante Iscriviti, la pagina verrà reindirizzata come segue:

I risultati sono quelli attesi. Puoi modificare il parametro sammy nell'URL in sammy2 e vedrai che verrai reindirizzato alla pagina seguente.

Questo testa le protezioni del percorso che hai impostato nella tua applicazione.

Conclusione

In questo tutorial hai imparato come creare un'applicazione Next.js. Hai appreso l'architettura di un'applicazione Next.js e come utilizza il routing basato su file per creare percorsi dell'applicazione. Quindi, hai imparato a creare percorsi dinamici nella tua applicazione.

Successivamente, sei passato alla configurazione di un sistema di autenticazione utente per applicare le protezioni del percorso ai tuoi percorsi dinamici. Hai creato un'API REST per gestire la registrazione degli utenti e il recupero dei dettagli dell'utente, con l'autenticazione. Hai imparato ad accedere a questa API e a creare percorsi dinamici per visualizzare le informazioni dell'utente sul percorso dinamico.

L'autore ha selezionato la OWASP Foundation per ricevere una donazione come parte del programma Write for DOnations.

Articoli correlati: