Sicherheit für Single-Page-Anwendungen (SPA) – Erweiterte Integration

Dieser Artikel beschreibt einen Ansatz zur Integration von CrowdHandler in eine Single-Page-Anwendung (SPA), um diese vor übermäßigem Datenverkehr zu schützen und ein reibungsloses Nutzererlebnis zu gewährleisten. Die Integration besteht aus zwei Hauptkomponenten:

  1. Die JavaScript-Integration von CrowdHandler ist im SPA-Modus aktiviert.

  2. Eine maßgeschneiderte serverseitige Integration zum Schutz der API(s), auf der bzw. denen Ihre SPA basiert.

Die JavaScript-Integration dient als erste und wichtigste Schutzebene. Sie ist dafür zuständig, Benutzeranfragen zu prüfen, den Status der Werbeaktion im Browser zu verwalten und die Benutzer bei Bedarf in den Warteraum weiterzuleiten.

Die serverseitige Integration dient als zweite Schutzebene, die vor Personen schützt, die versiert genug sind, die JavaScript-Integration zu umgehen, und ist zudem dafür zuständig, Leistungsdaten an den CrowdHandler zu übermitteln.

Installation der JavaScript-Integration

Der erste Schritt besteht darin, unsere JavaScript-Integration mit aktiviertem SPA-Modus zu installieren.

Standardmäßig finden CrowdHandler-Prüfungen nur bei einem vollständigen DOM-Neuladen statt, d. h. wenn der Browser hart aktualisiert wird oder wenn eine Seite zum ersten Mal von Ihrem Webserver abgerufen wird, bevor das Anwendungsbundle heruntergeladen wird. In SPA-Anwendungen hat dies zur Folge, dass Nutzer nach ihrem ersten Aufruf für CrowdHandler „unsichtbar“ werden.

Der SPA-Modus löst dieses Problem, indem er zusätzliche Funktionen auslöst, die dazu führen, dass CrowdHandler-Prüfungen bei jeder Änderung der URL durchgeführt werden, unabhängig davon, ob ein DOM-Reload stattgefunden hat oder nicht. Dies wird erreicht, indem der URL-Status verfolgt wird und mithilfe eines Ereignis-Listeners bei jeder erkannten Änderung eine CrowdHandler-Prüfung erzwungen wird.

Schutz Ihrer API(s)

Wie Sie Ihre API mit CrowdHandler konkret schützen, hängt von der von Ihnen verwendeten Sprache bzw. dem verwendeten Framework ab, sodass es nicht möglich ist, alle Szenarien in diesem Leitfaden abzudecken. Einige konkrete Implementierungsbeispiele für NodeJS- und Lambda@Edge-Umgebungen (CloudFront) sind am Ende des Artikels verlinkt.

1. Fügen Sie Ihren API-Anfrage-Payloads zusätzliche Felder hinzu.

Nehmen wir für dieses Beispiel einmal an, Sie verwalten eine E-Commerce-SPA. Es gibt eine einzige API, die Ihrer Kontrolle unterliegt und zum Abrufen von Daten aufgerufen wird.

Wir gehen von folgenden Annahmen aus, aber unsere Beispiele lassen sich problemlos an Ihre Bedürfnisse anpassen:

  • Die Nutzdaten werden mit dem Inhaltstyp „application/json“ übermittelt.
  • Sie sind lediglich daran interessiert, die PUT- und POST-Methoden zu schützen. Dies umfasst in der Regel Vorgänge wie „In den Warenkorb legen“ und „Zur Kasse gehen“, was ausreicht, um zu verhindern, dass Angreifer, die die JavaScript-Integration umgehen, den gesamten Kaufprozess abschließen können. *

* Nichts hindert Sie daran, alle API-Aufrufe bzw. alle Arten von Anfrage-Methoden zu schützen. Dies kann sinnvoll sein, wenn Sie befürchten, dass böswillige Akteure es beispielsweise auf lastintensive API-Routen abgesehen haben, die auf GET-Methoden reagieren. Sie müssen die zusätzlichen Felder als Parameter in der Abfragezeichenfolge hinzufügen und extrahieren.

Felder

Schlüssel: sourceURL

Wert: location.href (oder gleichwertig)

Schlüssel: chToken

Wert: lokaler Speicher Crowdhandler-Token *

* Hier ist eine einfache Beispielfunktion, die das CrowdHandler-Token aus dem lokalen Speicher abruft. Ersetzen Sie „my.domain.com“ durch die Domain Ihrer Website und übermitteln Sie leere Zeichenfolgen „“ zurück, falls kein Token gefunden wird. Dies ist wichtig, da dadurch dem serverseitigen Code mitgeteilt wird, dass eine neue CrowdHandler-Sitzung zugewiesen werden soll.

//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. Serverseitigen Code installieren

Der serverseitige Code dient dazu, als Frontend für Ihre API zu fungieren und Anfragen anhand des CrowdHandler-Status zu überprüfen. API-Aufrufe, bei denen keine beförderte CrowdHandler-Sitzung vorliegt, sollten sofort unterbunden werden.

Der Wert „sourceURL“, den Sie in Ihren API-Payloads angegeben haben, wird beim Einchecken bei CrowdHandler als temporäre URL verwendet. Im Kontrollpanel haben Sie CrowdHandler so konfiguriert, dass die URLs Ihrer Website geschützt werden, nicht jedoch Ihre API-URLs. Diese temporäre Umschreibung unter Verwendung des „sourceURL“-Werts teilt CrowdHandler mit, von welcher Seite der API-Aufruf stammt.

Das CrowdHandler-Token wird aus dem chToken -Wert extrahiert, den Sie in Ihren API-Payloads angegeben haben.

Weitere Details zur Implementierung finden Sie in den Kommentaren im Code.

Beispiel – 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;

Beispiel – Lambda@Edge

Zuschaueranfrage

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

Antwort von 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. Noch einen Schritt weiter gehen...

Die oben genannten Beispiele sind relativ einfache Lösungen, um den Datenverkehr zu Ihrer API für Nutzer zu blockieren, die von CrowdHandler als nicht autorisiert eingestuft werden.

Wenn Sie den Benutzern, die direkt auf Ihre APIs zugreifen, entgegenkommen möchten oder Bedenken hinsichtlich Sonderfällen haben, können Sie den Beispielcode so anpassen, dass er eine JSON-Antwort zurückgibt, die eine vollständige URL für den Warteraum enthält. In unserer JS-SDK-Dokumentation erfahren Sie, wie Sie diese URL abrufen können.

Wenn Sie die vollständige URL des Warteraums zur Hand haben, könnten Sie diese in der Antwort angeben und Ihren clientseitigen Code die aktuelle URL in die URL des Warteraums umschreiben lassen.

Bitte beachten Sie: Dies muss clientseitig erfolgen. Das serverseitige Umschreiben der API-Anfragen entspricht im Wesentlichen der Rückgabe einer 403-Antwort und führt zu einer Umleitung der API-Aufrufe, nicht jedoch des Browsers des Nutzers.

4. Abschließende Anmerkungen

Wir hoffen zwar, dass die aufgeführten Beispiele verständlich und hilfreich sind, sind uns jedoch bewusst, dass Sie manchmal einen Spezialisten um Rat und Erläuterungen bitten müssen. Unsere Integrationsexperten stehen Ihnen unter support@crowdhandler.com zur Verfügung und helfen Ihnen gerne weiter, wo immer es nötig ist.