AJAX, jQuery & wie man die Same-Origin-Policy umgeht

AJAX und Cross-DomainDer heutige Beitrag soll einen kurzen Einstieg in das Thema AJAX mit jQuery geben sowie das Thema Cross Domain behandeln und aufzeigen, wie man mit kleinen Tricks auch AJAX Requests über mehrere Domains hinweg absetzen kann. Bevor wir jedoch beginnen, gibt es noch mal einen Einstieg in das Thema im Schnelldurchgang. Wer bereits fit im Thema ist und nur an dem AJAX Cross-Domain-Workaround interessiert ist, kann den ersten Teil des Artikels getrost überspringen.

Was ist AJAX und wie funktioniert es in jQuery?

Ajax ist eine Abkürzung und steht für “Asynchronous JavaScript And XML”. Es ist entgegen der Meinung einiger Leute im Internet keine Programmiersprache, sondern eher ein Pattern, also eine Art etwas zu Programmieren. Mittels AJAX lassen sich Webinhalte per Javascript, also Client-seitig im Browser, abrufen. Ich sage bewusst Inhalte, denn entgegen dem Namen “… And XML” lassen sich nicht nur XML-Dateien, sondern auch JSON, Plaintext und andere denkbare Inhalte laden.

synchrounous requestsUnd wozu braucht man AJAX nun? In den “Anfangszeiten” des Internets (und auf schlecht programmierten Webseiten noch Heute), wird eine Webseite im Browser jedes mal neu geladen, wenn ein Inhalt vom Server geholt werden soll. Als einfachstes Beispiel nehmen wir mal ein kleines Login-Formular. (Vorab folgender Hinweis: Der nachfolgende Login-Mechanismus dient nur zur Veranschaulichung. Sichere Logins programmiert man anders! Mehr dazu habe ich hier aufgeschrieben.)

Nun könnten wir also ein PHP-Skript schreiben, welches wie folgt aussieht:

<html>
<head>
    <title>Login</title>
</head>
<body>
    <form method="post" action="">
        <input type="password" id="passwd" name="pass">
        <input type="submit">
    </form>
<?php
if (isset($_POST["pass"])){
     
    $password = $_POST["pass"];
    if ($password == "secure"){
        echo "You are logged in.";
    }
    else {
        echo "Wrong password.";
    }
}
?>
</body>
</html>
</html>

(Eine Demo des Scripts findet ihr hier: login.php-Demo)

Wird das Skript geladen, zeigt es ein Login-Formular und prüft im PHP-Bereich des Skripts, ob die Post-Variable “pass” gesetzt ist. Beim ersten Aufruf des Skripts, ist sie nicht gesetzt, weshalb nichts weiteres passiert. Gibt man ein Passwort ein und drückt “Senden”, schickt das Skript die Formulareingabe an sich selbst. Diesmal die die Post-Variable durch das Formular gesetzt. Das Script wertet nun das übersendete Passwort aus und prüft ob es “secure” lautet.

Inhalte senden, ohne die Seite neu zu laden

Javascript AsyncDas oben gezeigte Script funktioniert zwar, erwirkt aber, dass die Seite jedes mal komplett neu geladen wird. Das mag bei solch einem simplen Beispiel noch gehen, bei einer echten Webseite ist dies jedoch mehr als nervig als auch zeitaufwändig. Dieses Problem können wir nun mittels AJAX umgehen.

Statt das ganze Formular an sich selbst zu senden und die Seite neuzuladen, können wir per AJAX nur das Passwort an den Server übergeben und das Prüfergebnis auswerten. Hierzu trennen wir zuerst die Passwort-Logik vom Formular und lagern diese in ein zweites Script namens login-check.php aus:

<?php
if (isset($_GET["pass"])){
     
    $password = $_GET["pass"];
    if ($password == "secure"){
        echo "You are logged in.";
    }
    else {
        echo "Wrong password.";
    }
}
?>

Der Einfachheit halber haben wir den Modus von Post auf Get umgestellt. Nun lässt sich das Script mit dem Passwort aufrufen und gibt dann den Status zurück. Die Demo findet ihr hier: login-check.php. Ein Aufruf könnte wie folgt aussehen:

https://code-bude.net/downloads/ajax-crossdomain/login-check.php?pass=secure

Wir haben nun ein getrenntes Script zum überprüfen der Passwort-Eingabe. Dieses wollen wir nun in unserem eigentlichen Login-Formular per AJAX aufrufen. Um den Aufruf zu vereinfachen nutzen wir die jQuery-Bibliothek und hier insbesondere dessen “get”-Funktion.

<html>
<head>
    <title>Login</title>
    <script src="https://code.jquery.com/jquery-1.11.3.js"></script>
</head>
<body>
    <input type="password" id="passwd" name="pass">
    <input type="button" id="btn" value="Check">
    <span id="info"><span>
 
    <script type="text/javascript">      
        $('#btn').click(function(){
            var passValue = $('#passwd').val();        
            $.get('login-check.php', { pass: passValue }, function(data){          
                $('#info').html(data);         
            });        
        });
    </script>
</body>
</html>

Am Login-Formular haben wir zuerst die Referenz auf die jQuery-Bibliothek hinzugefügt. Weiter haben wir den Button vom Typ “submit” auf “button” geändert, da wir kein Formular im klassischen Sinne mehr abschicken wollen. Abschließend haben wir den PHP-Teil entfernt und durch Javascript ersetzt. Das Script hängt sich per “click” an das Click-Event des “Check”-Buttons. Wird er gedrückt wird per “val()” der Wert des Passwort-Felds in der Variable “passValue” gespeichert. Abschließend wird der Wert per “get”-Funktion an unser login-check.php-Script gesendet. Die Antwort des Scripts wird in der data-Variable abgefangen und per “html”-Funktion im span-Tag ausgegeben.
Eine Demo des AJAX-Formulars findet ihr hier: login-ajax.html.

AJAX Debugging Developer ToolsKlick man nun auf den Check-Button, wird das Passwort im Hintergrund an das Login-Check-Script gesendet und die Antwort dann, ohne Neuladen der gesamten Seite, ausgegeben. Wer wissen möchte, was im Hintergrund geschieht, drückt vor dem Klick auf “Check” einfach mal die F12-Taste während er im Webbrowser ist. Diese öffnet die Entwickler-Tools, in denen man das Absetzen des Requests beobachten kann.

Neben stehender Screenshot zeigt wie das Passwort an das zweite Script gesendet wird. Gelb markiert ist die Übergabe per Get-Parameter. Im Response-Reiter (ebenfalls gelb markiert) lässt sich die Antwort des zweiten Scripts anschauen.

Das leidige Thema Cross-Domain und die SOP

Kommen wir nun zum Thema Cross-Domain-Ajax und damit auch zur sogenannten Same-Origin-Policy (=SOP). Die Same-Origin-Policy ist ein Sicherheitskonzept, welches von allen modernen Browsern implementiert ist und dafür sorgt, dass nur Seiten des gleichen Ursprungs (=Origin) per AJAX gerufen werden dürfen. Hierdurch soll unter anderem dem Nutzer versichert werden, dass alle Inhalte, die er sieht, auch wirklich von der Seite stammen, dessen Webseite er gerade geöffnet hat. Unser oben stehendes Beispiel ist davon nicht betroffen, da unser Ajax-Aufruf einen relativen Pfad hat (“login-check.php”) und somit auf den selben Server/die selbe Domain zeigt.

Erweitern wir unser Beispiel nun wie folgt, zeigt sich jedoch das ganze “Drama”. Gehen wir davon aus, dass wir einen zentralen Passwort-Verwaltungsserver haben, der von mehreren Webanwendungen genutzt werden soll. Auf diesem liegt nun auch das login-check.php-Script, dass die Passwörter überprüft. Für unser Beispiel sei dies der Server, der unter raffaelherrmann.de erreichbar ist. Das Script unter folgender Adresse erreichbar:

https://raffaelherrmann.de/demos/login-check.php?pass=secure

Nun passen wir unser Login-Script in Zeile 14 an und tauschen die Adresse aus:

//alt
//$.get('login-check.php', { pass: passValue }, function(data){
 
//neu
$.get('https://raffaelherrmann.de/demos/login-check.php', { pass: passValue }, function(data){ 

Beim Aufruf unseres Login-Script bzw. beim Klick auf den Button, werden wir feststellen, dass sich nichts tut. Ein Blick in die Entwickler-Tools des Browsers zeigt uns, dass die Same-Origin-Policy zugeschlagen hat. Schließlich liegt unser Login-Script mit “code-bude.net” auf einer anderen Domain als das Check-Script auf “raffaelherrmann.de”.
Eine Demo kann hier eingesehen werden: login-ajax-sop.html

Wie die folgenden Screenshots zeigen, war der Request mit einem Http-Status 200 zwar an sich erfolgreich, jedoch wurde die Abfrage dennoch durch den Browser geblockt. Die Fehlerkonsole zeigt die Same-Origin-Policy-Fehlermeldung.

AJAX Same-Origin-Policy Example-1  AJAX Same-Origin-Policy Example-2

Schauen wir uns zum Abschluss nun an, wie man die SOP umgehen kann und was für Wege es gibt, AJAX auch Cross-Domain (also über mehrere Domains hinweg) zu nutzen.

AJAX Cross-Domain mit CORS

javascript corsUm nun doch über mehrere Domains arbeiten zu können, gibt es mindestens zwei Varianten. Variante 1, welche empfehlenswerter ist, ist das setzen eines bestimmten Http-Headers. Denn wie wir in den Screenshots sehen konnten, findet die Anfrage am raffaelherrmann.de-Server statt, obwohl die SOP gezogen hat. Dies geschieht aus folgendem Grund: Antwortet der per AJAX angefragte Server mit einem bestimmten Header ist der Abruf erlaubt und die Same-Origin-Policy greift nicht ein. Dieses Verfahren nennt sich “Cross-Origin Resource Sharing” (kurz: CORS) und basiert im Wesentlichen auf dem sogenannten “Access-Control-Allow-Origin”-Header. Um unser Script nun also lauffähig zu bekommen, müssen wir dem login-check.php-Script diesen Header verpassen.

<?php
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST');
 
if (isset($_GET["pass"])){
     
    $password = $_GET["pass"];
    if ($password == "secure"){
        echo "You are logged in.";
    }
    else {
        echo "Wrong password.";
    }
}
?>

Zudem haben wir noch einen zweiten Header, den “Access-Control-Allow-Methods”-Header genutzt, welcher angibt, für welche Methoden die “Sperre aufgehoben” wird. Das angepasste Script liegt nun hier: login-check-cors.php

Passen wir nun noch das Ziel in unserer Login-Seite auf die neue Version des Login-Scripts an, so gelingt auch der Login.

//alt
//$.get('https://raffaelherrmann.de/demos/login-check.php', { pass: passValue }, function(data){   
 
//neu
$.get('https://raffaelherrmann.de/demos/login-check-cors.php', { pass: passValue }, function(data){

Die funktionierende Demo findet ihr hier: login-ajax-cors.html

AJAX Cross-Domain mit “PHP-Proxy”

proxy-scriptDie eben gezeigte Lösung mittels CORS-Headern hat jedoch einen großen Nachteil: Man benötigt Zugriff auf das aufzurufende Script. In manchen Fällen, besonders im geschäftlichen Umfeld, ist gar nicht oder nur unter hohem bürokratischen Aufwand möglich. In diesen Fällen bleibt noch die Möglichkeit eines “Proxies”. (Ich schreibe dies bewusst in Anführungszeichen, weil die folgende Lösung keinen vollwertigen Proxyserver meint, sondern im Sinne eines Proxy-Server Verbindungen kapselt.)

Muss man also auf eine Datei auf einem anderen Server zugreifen, aber die Same-Origin-Policy verbietet es, so bleibt noch die Möglichkeit ein Skript auf dem eigenen Server abzulegen, welches die Daten vom fremden Server abruft bzw. an den fremden Server schickt. Hier durch wird die SOP umgangen, weil das AJAX-Script ja das “Proxy-Script” auf dem eigenen Server aufruft. Das gerufene Proxy-Script läuft serverseitig und darf deshalb beliebige Ressourcen, auch die des fremden Servers, laden.

Ein Beispiel für solch ein Proxy-Script, dass für unser heutiges Beispiel passt, könnte wie folgt aussehen:

<?php
if (isset($_GET["pass"])){ 
    $password = $_GET["pass"];
    echo file_get_contents("http://raffaelherrmann.de/demos/login-check.php?pass=".$password);
}
?>

Eine Demo des Proxy-Scripts findet sich hier: proxy.php

Das Script ist schnell erklärt. Es nimmt den Parameter “pass” an und ruft mit diesem Parameter über die Funktion “file_get_contents” das entfernte login-check.php-Script auf. Auf diese Weise haben wir elegant das Same-Origin-Policy-Problem umgangen.

Eine Anmerkung zum Schluss: Das eben gezeigte Proxy-Script funktioniert natürlich nur für unser heutiges Beispiel und muss für den Einsatz in anderen Szenarien angepasst werden. Weiter sollte man sich bewusst sein, dass diese zweite Variante nicht immer auf Gegenliebe stößt, da sie die SOP umgeht. Schließlich ist einer der Zwecke der SOP auch, dass nicht ungefragt Daten von fremden Server geladen werden sollen. Aus großer Macht folgt auch große Verantwortung. Also nutzt das Wissen bitte mit dieser Mahnung im Hinterkopf!

1 Kommentare

  1. Franzsays:

    Hallo und Dankeschön für die klasse Erklärung. Habe Ajax bereits verwendet, aber mit den Code immer irgendwie zusammengeschuster, was im Endeffekt irgendwie geklappt hat. Habe es immer mit trial and error versucht aber jetzt verstehe ich endlich die Zusammenhänge. Man muss aber schon sagen, dass AJAX recht geschickt ist. So kann man alle eingegebenen Daten behalten, wenn man neuen Content läd.

Schreibe einen Kommentar zu Franz Antworten abbrechen

Deine E-Mail-Adresse wird nicht veröffentlicht. Sie dient nur dem Spamschutz.