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 *
* 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["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
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.