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
Aunque esperamos que los ejemplos proporcionados sean claros y útiles, comprendemos que a veces necesite hablar con un especialista para que le asesore y le aclare las dudas. Nuestros expertos en integración están a su disposición en support@crowdhandler.com para ayudarle en lo que necesite.