Protección de una aplicación de página única (SPA) - Integración avanzada
Este artículo describe un método para integrar CrowdHandler en una aplicación de página única (SPA) con el fin de protegerla frente a un tráfico excesivo y garantizar una experiencia de usuario fluida. La integración consta de dos componentes principales:
-
Integración de CrowdHandler con JavaScript con el modo SPA activado.
-
Una integración personalizada del lado del servidor que protege la API (o las APIs) que dan servicio a tu SPA.
La función de la integración de JavaScript es actuar como primera y principal capa de protección, encargándose 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.
La función de la integración del lado del servidor es actuar como una segunda capa de protección, protegiendo contra cualquier persona lo suficientemente experta como para eludir la integración de JavaScript, además de encargarse de proporcionar información sobre el rendimiento a CrowdHandler.
Instalación de la integración de JavaScript
El primer paso es instalar nuestra integración de JavaScript con el modo SPA activado
De forma predeterminada, las comprobaciones de CrowdHandler solo se realizarán tras una recarga completa del DOM, es decir, cuando se actualiza el navegador de forma forzada o cuando se carga una página por primera vez desde el servidor web, antes de que se descargue el paquete de la aplicación. En las aplicaciones SPA, esto hace que los usuarios «desaparezcan» para CrowdHandler tras su primera visita.
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 realizando un seguimiento del estado de la URL y utilizando un detector de eventos para forzar una comprobación de CrowdHandler cada vez que se detecta un cambio.
Proteger tus API
La forma concreta de proteger tu API con CrowdHandler depende del lenguaje o marco de trabajo que utilices, por lo que resulta imposible abarcar todos los casos de uso en esta guía. Al final del artículo encontrarás enlaces a algunos ejemplos concretos de implementación para entornos Node.js y Lambda@Edge (CloudFront).
1. Añade campos adicionales a las cargas útiles de tus solicitudes de API.
A efectos de este ejemplo, supongamos que gestionas una aplicación SPA de comercio electrónico. Existe una única API, bajo tu control, a la que se realiza una llamada para recuperar datos.
Vamos a partir de las siguientes premisas, aunque nuestros ejemplos se pueden adaptar fácilmente a tus necesidades:
- Las cargas útiles se envían utilizando el tipo de contenido «application/json».
- Solo te interesa proteger los métodos PUT y POST. Esto suele abarcar operaciones como «añadir a la cesta» y «finalizar la compra», lo cual es suficiente para impedir que quienes eluden la integración de JavaScript puedan completar los recorridos de principio a fin. *
* No hay nada que te impida proteger todos los tipos de llamadas a la API y métodos de solicitud, y esto puede resultar adecuado si te preocupa que haya personas malintencionadas que se centren, por ejemplo, en rutas de la API con gran volumen de tráfico que respondan a métodos GET. Tendrás que añadir y extraer los campos adicionales como parámetros de la cadena de consulta.
Campos
Clave: sourceURL
Valor: location.href (o equivalente)
Clave: chToken
Valor: almacenamiento local token de Crowdhandler *
* A continuación se muestra una función de ejemplo sencilla que extrae el token de CrowdHandler del almacenamiento local. Sustituye «my.domain.com» por el dominio de tu sitio web y envía cadenas vacías «» si no se encuentra ningún token. Esto es importante, ya que indica al código del servidor que debe asignarse una nueva sesión de 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. Instalar el código del lado del servidor
El objetivo del código del lado del servidor es actuar como filtro ante tu API y validar las solicitudes con CrowdHandler para comprobar su estado de promoción. Las llamadas a la API que no presenten una sesión de CrowdHandler promocionada deben bloquearse de inmediato.
El valor de «sourceURL» que has proporcionado en las cargas útiles de tu API se utiliza como URL temporal al realizar el registro con CrowdHandler. En el panel de control, habrás configurado CrowdHandler para proteger las URL de tu sitio web, no las de tu API. Esta reescritura temporal, que utiliza el valor de «sourceURL», indica a CrowdHandler la página desde la que se originó la llamada a la API.
El token de CrowdHandler se extrae del valor «chToken» que has facilitado en las cargas útiles de la API.
Consulta 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({request: req, response: res});
const ch_gatekeeper = new crowdhandler.Gatekeeper(
public_client,
ch_context,
{ publicKey: 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();
}
/*
* IMPORTANT CONSIDERATION:
*
* The default status code sent to CrowdHandler is '200'. However, if a different status code needs to be sent,
* it can be achieved by passing it as a parameter to the 'recordPerformance' method.
*
* Example:
* chGatekeeper.recordPerformance({status: 404});
*
* If you are using CrowdHandler's autotune feature, it is crucial to pass accurate status codes to CrowdHandler to ensure the precision of analytics and autotune results.
*/
});
});
// Export the router
module.exports = router;
Ejemplo: Lambda@Edge

Solicitud de un 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;
};
Respuesta de Origin
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. Yendo un paso más allá...
Los ejemplos anteriores son soluciones relativamente sencillas para bloquear el tráfico hacia tu API por parte de usuarios que CrowdHandler considera no autorizados.
Si quieres facilitar las cosas a los usuarios que acceden directamente a tus API o te preocupan los casos excepcionales, puedes modificar el código de ejemplo para que devuelva una respuesta JSON que contenga una URL de la sala de espera completa. Consulta nuestra documentación del SDK de JS para ver cómo puedes obtener esta URL.
Si dispones de la URL completa de la sala de espera, podrías incluirla en la respuesta y hacer que tu código del lado del cliente reescriba la URL actual por la URL de la sala de espera.
¡Recuerda! Esto hay que hacerlo del lado del cliente. Reescribir las solicitudes a la API del lado del servidor equivale, en esencia, a devolver una respuesta 403 y redirigirá las llamadas a la API, no el navegador del usuario.
4. Notas finales
Aunque esperamos que los ejemplos que ofrecemos sean claros y útiles, entendemos que, en ocasiones, es necesario consultar a un especialista para obtener asesoramiento y aclaraciones. Nuestros expertos en integración están a tu disposición en support@crowdhandler.com y listos para ayudarte en lo que necesites.