Questo articolo illustra un approccio per integrare CrowdHandler con un'applicazione a pagina singola (SPA) per proteggerla dal traffico eccessivo e mantenere un'esperienza utente fluida. L'integrazione consiste in due componenti principali:
1.) Integrazione Javascript di CrowdHandler con modalità SPA abilitata.
2.) Un'integrazione personalizzata lato server che protegge l'API (o le API) che alimentano la vostra SPA.
Il ruolo dell'integrazione Javascript è quello di funzionare come primo e principale livello di protezione, responsabile del controllo delle richieste degli utenti, della gestione dello stato di promozione nel browser e del reindirizzamento degli utenti alla sala d'attesa, se necessario.
Il ruolo dell'integrazione lato server è quello di fungere da secondo livello di protezione, proteggendo da chiunque sia abbastanza abile da aggirare l'integrazione Javascript, oltre a essere responsabile dell'alimentazione delle informazioni sulle prestazioni di CrowdHandler.
Installazione dell'integrazione Javascript
Il primo passo consiste nell'installare la nostra integrazione Javascript con la modalità SPA abilitata
Per impostazione predefinita, i controlli di CrowdHandler vengono eseguiti solo in caso di ricarica completa del DOM, ossia quando il browser viene aggiornato o quando una pagina viene recuperata per la prima volta dal server web prima che venga scaricato il bundle dell'applicazione. Nelle applicazioni SPA, questo ha l'effetto di rendere gli utenti "oscuri" a CrowdHandler dopo il primo accesso.
La modalità SPA risolve questo problema attivando una funzionalità aggiuntiva che fa sì che i controlli di CrowdHandler vengano eseguiti ogni volta che l'URL cambia, indipendentemente dal fatto che si sia verificato o meno un ricaricamento del DOM. Ciò si ottiene tracciando lo stato dell'URL e utilizzando un ascoltatore di eventi per forzare un controllo di CrowdHandler ogni volta che viene rilevata una modifica.
Protezione delle API
Il modo specifico di proteggere la propria API con CrowdHandler dipende dal linguaggio/framework utilizzato, per cui è impossibile coprire tutti gli scenari in questa guida. Alcuni esempi specifici di implementazione per ambienti NodeJS e Lambda@Edge (CloudFront) sono riportati alla fine dell'articolo.
1.) Aggiungere campi aggiuntivi ai payload delle richieste API.
Ai fini di questo esempio, supponiamo di gestire una SPA di e-commerce. C'è una singola API, sotto il vostro controllo, che viene chiamata per recuperare i dati.
Presumiamo quanto segue, ma i nostri esempi possono essere facilmente adattati alle vostre esigenze:
- I payload vengono inviati utilizzando il tipo di contenuto application/json.
- L'utente è interessato a proteggere solo i metodi PUT e POST. Questo copre in genere operazioni come l'aggiunta al carrello e il checkout, ed è sufficiente per impedire ai bypassatori dell'integrazione Javascript di completare i viaggi end-to-end. *
* Non c'è nulla che impedisca di proteggere tutte le chiamate API e i tipi di metodi di richiesta e questo può essere appropriato se si teme che i malintenzionati prendano di mira percorsi API ad alta intensità di carico che rispondono, ad esempio, a metodi GET. È necessario aggiungere ed estrarre i campi aggiuntivi come parametri della stringa di query.
Campi
Chiave: sourceURL
Valore: location.href (o equivalente)
Chiave: chToken
Valore: archiviazione locale 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["my.domain.com"] } catch (error) { return "" }
2.) Installare il codice lato server
Lo scopo del codice sul lato server è quello di posizionarsi di fronte all'API e convalidare le richieste rispetto a CrowdHandler per lo stato di promozione. Le chiamate API che non presentano una sessione CrowdHandler promossa devono essere interrotte.
Il valore sourceURL fornito nei payload API viene utilizzato come URL temporaneo al momento del check-in con CrowdHandler. Nel pannello di controllo, CrowdHandler è stato configurato per proteggere gli URL del sito web e non quelli delle API. Questa riscrittura temporanea utilizzando il valore sourceURL informa CrowdHandler della pagina da cui proviene la chiamata API.
Il token di CrowdHandler viene estratto dal valore chToken fornito nei payload API.
Vedere i commenti al codice per ulteriori dettagli sull'implementazione.
Esempio - Struttura 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;
Esempio - Lambda@Edge
Richiesta dello spettatore
"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; };
Risposta all'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.) Per andare oltre...
Gli esempi sopra riportati sono soluzioni relativamente semplici per bloccare il traffico verso la propria API per gli utenti considerati non autorizzati da CrowdHandler.
Se si vuole giocare d'anticipo con gli utenti che accedono direttamente alle proprie API o si è preoccupati per i casi limite, si può modificare il codice di esempio per restituire una risposta JSON contenente un URL della sala d'attesa completamente formato. Consultate la documentazione del nostro SDK JS per vedere come ottenere questo URL.
Con l'URL completo della sala d'attesa a portata di mano, è possibile inserirlo nella risposta e far sì che il codice lato client riscriva l'URL corrente nell'URL della sala d'attesa.
Ricordare! Questa operazione deve essere eseguita sul lato client; riscrivere le richieste API sul lato server equivale essenzialmente a restituire una risposta 403 e a reindirizzare le chiamate API, non il browser dell'utente.
4.) Note finali
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.