Questo articolo illustra un approccio per integrare CrowdHandler con un'applicazione a pagina singola (SPA) per proteggerla dal traffico eccessivo e mantenere un'esperienza utente fluida. L'integrazione consiste in due componenti principali:

1.) Integrazione Javascript di CrowdHandler con modalità SPA abilitata.

2.) Un'integrazione personalizzata lato server che protegge l'API (o le API) che alimentano la vostra SPA.

Il ruolo dell'integrazione Javascript è quello di funzionare come primo e principale livello di protezione, responsabile del controllo delle richieste degli utenti, della gestione dello stato di promozione nel browser e del reindirizzamento degli utenti alla sala d'attesa, se necessario.

Il ruolo dell'integrazione lato server è quello di fungere da secondo livello di protezione, proteggendo da chiunque sia abbastanza abile da aggirare l'integrazione Javascript, oltre a essere responsabile dell'alimentazione delle informazioni sulle prestazioni di CrowdHandler.

Installazione dell'integrazione Javascript

Il primo passo consiste nell'installare la nostra integrazione Javascript con la modalità SPA abilitata

Per impostazione predefinita, i controlli di CrowdHandler vengono eseguiti solo in caso di ricarica completa del DOM, ossia quando il browser viene aggiornato o quando una pagina viene recuperata per la prima volta dal server web prima che venga scaricato il bundle dell'applicazione. Nelle applicazioni SPA, questo ha l'effetto di rendere gli utenti "oscuri" a CrowdHandler dopo il primo accesso.

La modalità SPA risolve questo problema attivando una funzionalità aggiuntiva che fa sì che i controlli di CrowdHandler vengano eseguiti ogni volta che l'URL cambia, indipendentemente dal fatto che si sia verificato o meno un ricaricamento del DOM. Ciò si ottiene tracciando lo stato dell'URL e utilizzando un ascoltatore di eventi per forzare un controllo di CrowdHandler ogni volta che viene rilevata una modifica.

Protezione delle API

Il modo specifico di proteggere la propria API con CrowdHandler dipende dal linguaggio/framework utilizzato, per cui è impossibile coprire tutti gli scenari in questa guida. Alcuni esempi specifici di implementazione per ambienti NodeJS e Lambda@Edge (CloudFront) sono riportati alla fine dell'articolo.

1.) Aggiungere campi aggiuntivi ai payload delle richieste API.

Ai fini di questo esempio, supponiamo di gestire una SPA di e-commerce. C'è una singola API, sotto il vostro controllo, che viene chiamata per recuperare i dati.

Presumiamo quanto segue, ma i nostri esempi possono essere facilmente adattati alle vostre esigenze:

  • I payload vengono inviati utilizzando il tipo di contenuto application/json.
  • L'utente è interessato a proteggere solo i metodi PUT e POST. Questo copre in genere operazioni come l'aggiunta al carrello e il checkout, ed è sufficiente per impedire ai bypassatori dell'integrazione Javascript di completare i viaggi end-to-end. *

* Non c'è nulla che impedisca di proteggere tutte le chiamate API e i tipi di metodi di richiesta e questo può essere appropriato se si teme che i malintenzionati prendano di mira percorsi API ad alta intensità di carico che rispondono, ad esempio, a metodi GET. È necessario aggiungere ed estrarre i campi aggiuntivi come parametri della stringa di query.

Campi

Chiave: sourceURL

Valore: location.href (o equivalente)

Chiave: chToken

Valore: archiviazione locale token crowdhandler *

* Ecco una semplice funzione di esempio che estrae il token di CrowdHandler dalla memoria locale. Sostituire my.domain.com con il dominio del sito e inviare la stringa vuota "" se non viene trovato alcun token. Questo è importante perché informa il codice lato server che deve essere assegnata una nuova sessione di CrowdHandler. 

//Storage format
'{"countdown":{},"positions":{},"token":{"my.domain.com":"tok0N53DjDMpWeid"}}'
try {
  let ch_storage = JSON.parse(localStorage.getItem("crowdhandler"))
  return ch_storage.token["my.domain.com"]
} catch (error) {
  return ""
}

2.) Installare il codice lato server

Lo scopo del codice sul lato server è quello di posizionarsi di fronte all'API e convalidare le richieste rispetto a CrowdHandler per lo stato di promozione. Le chiamate API che non presentano una sessione CrowdHandler promossa devono essere interrotte.

Il valore sourceURL fornito nei payload API viene utilizzato come URL temporaneo al momento del check-in con CrowdHandler. Nel pannello di controllo, CrowdHandler è stato configurato per proteggere gli URL del sito web e non quelli delle API. Questa riscrittura temporanea utilizzando il valore sourceURL informa CrowdHandler della pagina da cui proviene la chiamata API.

Il token di CrowdHandler viene estratto dal valore chToken fornito nei payload API.

Vedere i commenti al codice per ulteriori dettagli sull'implementazione.

Esempio - Struttura Express

const express = require("express");
const router = express.Router();
const crowdhandler = require("crowdhandler-sdk");
const { URL } = require("url");

// Middleware to handle CrowdHandler logic for POST and PUT methods
const crowdHandlerMiddleware = async (req, res, next) => {
  const method = req.method;

  // Check if the request method is POST or PUT
  if (method === "POST" || method === "PUT") {
    const publicKey = "YOUR_PUBLIC_KEY";
    const public_client = new crowdhandler.PublicClient(publicKey);
    const ch_context = new crowdhandler.RequestContext(req, res);
    const ch_gatekeeper = new crowdhandler.Gatekeeper(
      public_client,
      ch_context,
      publicKey
    );

    let decodedBody;
    let chToken;
    let sourceURL;

    if (req.body) {
      try {
        decodedBody = JSON.parse(req.body);
        chToken = decodedBody.chToken;
        sourceURL = decodedBody.sourceURL;

        // Extract host & path from sourceURL
        let url = new URL(sourceURL);
        let temporaryHost = url.host;
        let temporaryPath = url.pathname;

        // Override the gatekeeper host and path with the sourceURL
        ch_gatekeeper.overrideHost(temporaryHost);
        ch_gatekeeper.overridePath(temporaryPath);

        // If there's a token in the body, provide gatekeeper with a pseudo cookie
        if (chToken) {
          ch_gatekeeper.overrideCookie(`crowdhandler=${chToken}`);
        }
      } catch (error) {
        console.error("Error parsing JSON:", error);
        return next(error);
      }
    }

    const ch_status = await ch_gatekeeper.validateRequest();

    // If the request is not promoted, send a 403 Forbidden response and do not proceed to the next middleware
    if (!ch_status.promoted) {
      res.status(403).send("Forbidden");
      return;
    } else {
      // If the request is promoted, save the ch_gatekeeper instance in res.locals for later use
      res.locals.ch_gatekeeper = ch_gatekeeper;
    }
  }
  // Continue to the next middleware or route handler
  next();
};

// Add the CrowdHandler middleware to the router
router.use(crowdHandlerMiddleware);

// Route handler for all request methods and paths
router.all("*", (req, res, next) => {
  // Render the view and send the HTML
  res.render("index", { title: "hello" }, (err, html) => {
    // Handle any errors during rendering
    if (err) {
      return next(err);
    }

    // Send the rendered HTML to the client
    res.send(html);

    // If the ch_gatekeeper instance exists in res.locals, record the performance
    if (res.locals.ch_gatekeeper) {
      res.locals.ch_gatekeeper.recordPerformance();
    }
  });
});

// Export the router
module.exports = router;

Esempio - Lambda@Edge

Richiesta dello spettatore

"use strict";
//include crowdhandler-sdk
const crowdhandler = require("crowdhandler-sdk");
const publicKey = "YOUR_PUBLIC_KEY_HERE";

let ch_client = new crowdhandler.PublicClient(publicKey, { timeout: 2000 });

module.exports.viewerRequest = async (event) => {
  //extract the request from the event
  let request = event.Records[0].cf.request;
  let decodedBody;
  let chToken;
  let sourceURL;

  //if the request is not a POST or PUT request, return the request unmodified
  if (request.method !== "POST" || request.method !== "PUT" ) {
    return request;
  }

  if (request.body && request.body.encoding === "base64") {
    // Decode the base64 encoded body
    decodedBody = Buffer.from(request.body.data, "base64").toString("utf8");

    // Parse the JSON encoded body
    try {
      // Parse the decoded body into a JSON object
      decodedBody = JSON.parse(decodedBody);
      //destructure sourceURL, chToken from the decoded body
      chToken = decodedBody.chToken;
      sourceURL = decodedBody.sourceURL;

      // Now you can work with the JSON object
    } catch (error) {
      console.error("Error parsing JSON:", error);

      // Handle the error or return the request object unmodified
      return request;
    }
  }

  //extract host & path from sourceURL using URL API
  let url = new URL(sourceURL);
  let temporaryHost = url.host;
  let temporaryPath = url.pathname;

  //Filter the event through the Request Context class
  let ch_context = new crowdhandler.RequestContext({ lambdaEvent: event });
  //Instantiate the Gatekeeper class
  let ch_gatekeeper = new crowdhandler.Gatekeeper(
    ch_client,
    ch_context,
    {
      publicKey: publicKey,
    },
    { debug: true }
  );

  //Override the gatekeeper host with the sourceURL
  ch_gatekeeper.overrideHost(temporaryHost);
  //Override the gatekeeper path with the sourceURL
  ch_gatekeeper.overridePath(temporaryPath);

  //If there's a token in the body provide gatekeeper with a pseudo cookie so that it can check that the provided token is valid/promoted
  if (chToken) {
    ch_gatekeeper.overrideCookie(`crowdhandler=${chToken}`);
  }

  //Validate the request
  let ch_status = await ch_gatekeeper.validateRequest();

  //If the request is not promoted, reject the request
  if (!ch_status.promoted) {
    return {
      status: "403",
      statusDescription: "Forbidden",
      headers: {
        "content-type": [
          {
            key: "Content-Type",
            value: "text/plain",
          },
        ],
        "cache-control": [
          {
            key: "Cache-Control",
            value: "max-age=0",
          },
        ],
      },
      body: "Access to this resource is forbidden.",
    };
  }

  //If the request is promoted, allow it to proceed normally
  //set customer headers for recording performance on the request before passing it through
  request.headers["x-crowdhandler-responseID"] = [
    { key: "x-crowdhandler-responseID", value: `${ch_status.responseID}` },
  ];
  request.headers["x-crowdhandler-startTime"] = [
    { key: "x-crowdhandler-startTime", value: `${Date.now()}` },
  ];

  //return the request
  return request;
};

Risposta all'origine

const crowdhandler = require("crowdhandler-sdk");
const publicKey = "YOUR_PUBLIC_KEY_HERE";

let ch_client = new crowdhandler.PublicClient(publicKey, { timeout: 2000 });

module.exports.originResponse = async (event) => {
  let request = event.Records[0].cf.request;
  let requestHeaders = event.Records[0].cf.request.headers;
  let response = event.Records[0].cf.response;
  let responseStatus = response.status;

  //convert response status to number
  responseStatus = parseInt(responseStatus);

  //extract the custom headers that we passed through from the viewerRequest event
  let responseID;
  let startTime;

  try {
    responseID = requestHeaders["x-crowdhandler-responseid"][0].value;
  } catch (e) {}

  try {
    startTime = requestHeaders["x-crowdhandler-starttime"][0].value;
  } catch (e) {}

  //Work out how long we spent processing at the origin
  let elapsed = Date.now() - startTime;

  let ch_context = new crowdhandler.RequestContext({ lambdaEvent: event });

  //Instantiate the Gatekeeper class
  let ch_gatekeeper = new crowdhandler.Gatekeeper(
    ch_client,
    ch_context,
    {
      publicKey: publicKey,
    },
    { debug: true }
  );

  //If we don't have a responseID or a startTime, we can't record the performance
  if (!responseID || !startTime) {
    return response;
  }

  //This is a throw away request. We don't need to wait for a response.
  await ch_gatekeeper.recordPerformance({
    overrideElapsed: elapsed,
    responseID: responseID,
    sample: 1,
    statusCode: responseStatus,
  });

  //Fin
  return response;
};

3.) Per andare oltre...

Gli esempi sopra riportati sono soluzioni relativamente semplici per bloccare il traffico verso la propria API per gli utenti considerati non autorizzati da CrowdHandler. 

Se si vuole giocare d'anticipo con gli utenti che accedono direttamente alle proprie API o si è preoccupati per i casi limite, si può modificare il codice di esempio per restituire una risposta JSON contenente un URL della sala d'attesa completamente formato. Consultate la documentazione del nostro SDK JS per vedere come ottenere questo URL.

Con l'URL completo della sala d'attesa a portata di mano, è possibile inserirlo nella risposta e far sì che il codice lato client riscriva l'URL corrente nell'URL della sala d'attesa.

Ricordare! Questa operazione deve essere eseguita sul lato client; riscrivere le richieste API sul lato server equivale essenzialmente a restituire una risposta 403 e a reindirizzare le chiamate API, non il browser dell'utente.

4.) Note finali

Ci auguriamo che gli esempi forniti siano chiari e utili, ma siamo consapevoli che a volte è necessario rivolgersi a uno specialista per ottenere consigli e chiarimenti. I nostri esperti di integrazione sono disponibili all'indirizzo support@crowdhandler.com e pronti a fornire assistenza in caso di necessità.