Cet article décrit une approche pour intégrer CrowdHandler à une application à page unique (SPA) afin de la protéger contre un trafic excessif et de maintenir une expérience utilisateur fluide. L'intégration se compose de deux éléments principaux :

1.) L'intégration Javascript de CrowdHandler avec le mode SPA activé.

2.) Une intégration personnalisée côté serveur protégeant l'API (ou les API) qui alimente votre SPA.

Le rôle de l'intégration Javascript est de fonctionner comme la première couche de protection, responsable de la vérification des demandes des utilisateurs, de la gestion de l'état de la promotion dans le navigateur et de la redirection des utilisateurs vers la salle d'attente si nécessaire.

Le rôle de l'intégration côté serveur est d'agir comme une deuxième couche de protection, en empêchant quiconque de contourner l'intégration Javascript et en alimentant CrowdHandler en informations sur les performances.

Installation de l'intégration Javascript

La première étape consiste à installer notre intégration Javascript avec le mode SPA activé

Par défaut, les vérifications de CrowdHandler n'ont lieu que lors d'un rechargement complet du DOM, c'est-à-dire lorsque le navigateur est rafraîchi ou lorsqu'une page est récupérée pour la première fois sur votre serveur web avant que le pack d'application ne soit téléchargé. Dans les applications SPA, cela a pour effet de rendre les utilisateurs "sombres" pour CrowdHandler après leur première visite.

Le mode SPA résout ce problème en déclenchant une fonctionnalité supplémentaire qui permet d'effectuer des vérifications CrowdHandler chaque fois que l'URL change, qu'il y ait eu ou non un rechargement du DOM. Ceci est réalisé en suivant l'état de l'URL et en utilisant un écouteur d'événement pour forcer une vérification CrowdHandler chaque fois qu'un changement est détecté.

Protéger votre (vos) API

La façon spécifique dont vous protégez votre API avec CrowdHandler dépend du langage/framework que vous utilisez, ce qui rend impossible de couvrir tous les scénarios dans ce guide. Certains exemples de mise en œuvre spécifiques pour les environnements NodeJS et Lambda@Edge (CloudFront) sont liés vers la fin de l'article.

1.) Ajoutez des champs supplémentaires aux données utiles de vos demandes d'API.

Pour les besoins de cet exemple, disons que vous gérez une SPA de commerce électronique. Il existe une API unique, sous votre contrôle, qui est appelée pour récupérer des données.

Nous allons supposer ce qui suit, mais nos exemples peuvent facilement être adaptés à vos besoins :

  • Les charges utiles sont soumises en utilisant le type de contenu application/json.
  • Vous souhaitez uniquement protéger les méthodes PUT et POST. Cela couvre généralement des opérations telles que l'ajout au panier et le paiement, ce qui est suffisant pour empêcher les contourneurs de l'intégration Javascript d'effectuer des visites de bout en bout. *

* Rien ne vous empêche de protéger tous les appels API/types de méthodes de requête, ce qui peut s'avérer utile si vous craignez que des acteurs malveillants ne ciblent des itinéraires API à forte charge qui répondent aux méthodes GET, par exemple. Vous devrez ajouter et extraire les champs supplémentaires en tant que paramètres de chaîne de requête.

Domaines

Clé : sourceURL

Valeur : location.href (ou équivalent)

Clé : chToken

Valeur : stockage local jeton crowdhandler *

* Voici un exemple simple de fonction qui extrait le jeton CrowdHandler du stockage local. Remplacez my.domain.com par le domaine de votre site et soumettez des chaînes vides "" si aucun jeton n'est trouvé. Ceci est important car il informe le code côté serveur qu'une nouvelle session CrowdHandler doit être assignée. 

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

2.) Installer le code côté serveur

L'objectif du code côté serveur est de s'asseoir devant votre API et de valider les requêtes contre CrowdHandler pour l'état de la promotion. Les appels à l'API qui ne présentent pas une session CrowdHandler promue doivent être interrompus.

La valeur sourceURL que vous avez fournie dans votre API payloads est utilisée comme URL temporaire lors de l'enregistrement avec CrowdHandler. Dans le panneau de contrôle, vous aurez configuré CrowdHandler pour protéger les URLs de votre site web, pas les URLs de votre API. Cette réécriture temporaire utilisant la valeur sourceURL informe CrowdHandler de la page d'où provient l'appel API.

Le jeton CrowdHandler est extrait de la valeur chToken que vous avez fournie dans vos données API.

Voir les commentaires du code pour plus de détails sur la mise en œuvre.

Exemple - Cadre 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;

Exemple - Lambda@Edge

Demande du téléspectateur

"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;
};

Réponse d'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.) Pour aller plus loin...

Les exemples ci-dessus sont des solutions relativement simples pour bloquer le trafic vers votre API pour les utilisateurs qui sont considérés comme non autorisés par CrowdHandler. 

Si vous souhaitez jouer franc jeu avec les utilisateurs qui accèdent directement à vos API ou si vous êtes préoccupé par les cas extrêmes, vous pouvez modifier le code de l'exemple pour renvoyer une réponse JSON contenant une URL de salle d'attente entièrement formée. Consultez la documentation de notre SDK JS pour savoir comment obtenir cette URL.

Si vous disposez de l'URL complète de la salle d'attente, vous pouvez la faire apparaître dans la réponse et demander à votre code côté client de réécrire l'URL actuelle en URL de la salle d'attente.

N'oubliez pas ! Cette opération doit être effectuée côté client, car la réécriture des demandes d'API côté serveur revient essentiellement à renvoyer une réponse 403 et à rediriger les appels d'API, et non le navigateur de l'utilisateur.

4.) Notes finales

Nous espérons que les exemples fournis sont clairs et utiles, mais nous comprenons qu'il est parfois nécessaire de s'adresser à un spécialiste pour obtenir des conseils et des éclaircissements. Nos experts en intégration sont disponibles à l'adresse support@crowdhandler.com et prêts à vous aider si nécessaire.