Este artículo describe un enfoque para integrar CrowdHandler con una aplicación de página única (SPA) para protegerla contra el tráfico excesivo y mantener una experiencia de usuario fluida. La integración consta de dos componentes principales:

1.) Integración Javascript de CrowdHandler con el modo SPA activado.

2.) Una integración personalizada del lado del servidor que protege la API (o las API) que alimenta su SPA.

El papel de la integración Javascript es funcionar como la primera y principal capa de protección, responsable de comprobar las solicitudes de los usuarios, gestionar el estado de la promoción en el navegador y redirigir a los usuarios a la sala de espera si es necesario.

El papel de la integración del lado del servidor es actuar como una segunda capa de protección, protegiendo contra cualquier persona lo suficientemente astuta como para eludir la integración de Javascript, además de ser responsable de alimentar la información de rendimiento de CrowdHandler.

Instalación de la integración de Javascript

El primer paso es instalar nuestra integración Javascript con el modo SPA activado

Por defecto, las comprobaciones de CrowdHandler sólo tienen lugar cuando se recarga todo el DOM, es decir, cuando se actualiza el navegador o cuando se obtiene una página por primera vez de su servidor web antes de que se descargue el paquete de la aplicación. En las aplicaciones SPA, esto tiene el efecto de que los usuarios "se quedan a oscuras" para CrowdHandler después de su visita inicial.

El modo SPA resuelve este problema activando una funcionalidad adicional que hace que se realicen comprobaciones de CrowdHandler cada vez que cambia la URL, independientemente de si se ha producido o no una recarga del DOM. Esto se consigue rastreando el estado de la URL y utilizando un receptor de eventos para forzar una comprobación CrowdHandler cada vez que se detecta un cambio.

Proteger su(s) API(s)

La forma específica de proteger su API con CrowdHandler depende del lenguaje/framework que utilice, por lo que es imposible cubrir todos los escenarios en esta guía. Algunos ejemplos de implementación específicos para entornos NodeJS y Lambda@Edge (CloudFront) están enlazados al final del artículo.

1.) Añada campos adicionales a sus cargas útiles de solicitud de API.

Para el propósito de este ejemplo, digamos que usted está administrando una SPA de comercio electrónico. Hay una única API, bajo su control, que se llama para obtener datos.

Vamos a suponer lo siguiente, pero nuestros ejemplos pueden adaptarse fácilmente a sus necesidades:

  • Las cargas se envían utilizando el tipo de contenido application/json.
  • Sólo le interesa proteger los métodos PUT y POST. Esto normalmente cubre operaciones como añadir a la cesta y pago que es suficiente para evitar que los bypassers de integración de Javascript puedan completar viajes de extremo a extremo. *

* No hay nada que le impida proteger todas las llamadas a la API/todos los tipos de métodos de solicitud y esto puede ser apropiado si le preocupan los malos actores que se dirigen a rutas de API de carga intensiva que responden a métodos GET, por ejemplo. Tendrá que añadir y extraer los campos adicionales como parámetros de cadena de consulta.

Campos

Clave: sourceURL

Valor: location.href (o equivalente)

Clave: chToken

Valor: almacenamiento local token crowdhandler *

* Here is a simple example function that extracts the CrowdHandler token from local storage. Replace my.domain.com with your site domain and submit empty strings "" if no token is found. This is important as it informs the server-side code that a fresh CrowdHandler>session should be assigned. 

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

2.) Instalar el código del lado del servidor

El propósito del código del lado del servidor es sentarse frente a su API y validar las solicitudes contra CrowdHandler para el estado de promoción. Las llamadas a la API que no presenten una sesión CrowdHandler promovida deben detenerse en seco.

El valor sourceURL que proporcionó en sus cargas útiles de API se utiliza como URL temporal al registrarse con CrowdHandler. En el panel de control, habrá configurado CrowdHandler para proteger las URL de su sitio web, no las URL de su API. Esta reescritura temporal utilizando el valor sourceURL informa a CrowdHandler de la página desde la que se originó la llamada a la API.

El token de CrowdHandler se extrae del valor chToken que proporcionó en sus cargas útiles de API.

Consulte los comentarios del código para obtener más detalles sobre la implementación.

Ejemplo - Express Framework

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;

Ejemplo - Lambda@Edge

Solicitud del espectador

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

Origen Respuesta

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.) Llevando las cosas más lejos...

Los ejemplos anteriores son soluciones relativamente sencillas para bloquear el tráfico a su API de usuarios que CrowdHandler considera no autorizados. 

Si quiere jugar limpio con los usuarios que acceden directamente a sus API o le preocupan los casos extremos, puede modificar el código de ejemplo para que devuelva una respuesta JSON que contenga una URL de sala de espera completamente formada. Consulte nuestra documentación JS SDK para ver cómo podría obtener esta URL.

Con la URL completa de la sala de espera a mano, puede mostrarla en la respuesta y hacer que el código del cliente reescriba la URL actual en la URL de la sala de espera.

¡Recuerde! Esto tiene que hacerse del lado del cliente, reescribir las peticiones API del lado del servidor es esencialmente lo mismo que devolver una respuesta 403 y redirigirá las llamadas API, no el navegador del usuario.

4.) Notas finales

While we hope that the examples provided are clear and useful, we understand that sometimes you need to speak to a specialist for advice and clarification. Our integration experts are available via [email protected] and ready to assist where needed.