• Strona główna
  • Curriculum Vitae
  • O mnie
  • PrzykÅ‚ad: Gramatyka w PHP
  • PrzykÅ‚ad: Kompresja CSS
  • PrzykÅ‚ad: Kompresja JavaScript
  • PrzykÅ‚ad: Skracanie linków
  • PrzykÅ‚ad: Wykrywanie serwera HTTP
  • PrzykÅ‚ad: WÅ‚asna bramka SMS
  • Mapa strony
  • Kontakt
Niebieski Pomarańczowy Zielony Różowy Fioletowy

Cross-Domain JavaScript: CORS

Opublikowane 10 października 2011. Autor: Kamil Brenk. Wizyt: 24 831.

Kategorie: JavaScript
Tematyka: bezpieczeństwo stron www, Cross-domain requests, Cross-Origin Resource Sharing, JavaScript, JSONP, obiekt XMLHttpRequest, Same origin policy, Yahoo YQL

paÅ 10

Ponad rok temu opisywałem na blogu różne techniki Cross-Domain JavaScript, czyli obejścia Same origin policy dla żądań XHR w języku JavaScript.

Czas powrócić do tematu i rozszerzyć zawartą tam wiedzę, bowiem konsorcjum W3C wyprowadziło nową specyfikację dla Cross-Origin Resource Sharing, a wszystkie nowoczesne przeglądarki zdążyły już ją zaimplementować, stąd mamy prostsze rozwiązanie problemu :)

Cross-Origin Resource Sharing, w czÄ™sto używanym skrócie CORS to technologia umożliwiajÄ…ca wykonywanie asynchronicznych połączeÅ„ do każdego miejsca w Sieci, o ile owe miejsce na to pozwoliÅ‚o. Znika wiÄ™c bariera Same origin policy i nie musimy używać tricków typu JSONP – możemy wykonywać żądania przy użyciu XMLHttpRequest poza naszÄ… domenÄ™.

Wymagania CORS

Aby móc wykorzystywać Cross-Origin Resource Sharing w swoim serwisie musi zostać spełnionych kilka warunków:

  • przeglÄ…darka musi obsÅ‚ugiwać CORS,
  • serwer, do którego siÄ™ odwoÅ‚ujemy musi być odpowiednio skonfigurowany,
  • kilka razy zastanawiamy siÄ™ czy naprawdÄ™ tego potrzebujemy :-) wzglÄ™dy bezpieczeÅ„stwa.

Przejdźmy do omówienia każdego z tych podpunktów.

Obsługa CORS przez przeglądarki

Na dzień dzisiejszy technologia Cross-Origin Resource Sharing jest obsługiwana w następujących przeglądarkach:

  • Firefox 3.5
  • Safari 4
  • Internet Explorer 8 (XDomainRequest, o czym dalej)
  • Google Chrome 3
  • iOS Safari 3.2
  • Android Browser 2.1
  • SeaMonkey 2.0

Niestety na powyższej liście nie znajdziemy Opery, więc dla tej przeglądarki (jak i innych, nie obsługujących CORS) musimy skorzystać z Browser Polyfills.

Nigdy nie wykrywaj wersji przeglÄ…darki – wykrywaj czy używana przez użytkownika przeglÄ…darka obsÅ‚uguje pożądanÄ… technologiÄ™!

Żeby wykryć obsługę CORS wystarczy następująca składnia:

1
2
3
4
var request = new XMLHttpRequest();
if ('withCredentials' in request) {
    // CORS supported (XHR)
}

lub:

1
2
3
4
var request = new XMLHttpRequest();
if (request.withCredentials !== undefined) {
    // CORS supported (XHR)
}

To tyle jeÅ›li chodzi o „normalne” przeglÄ…darki. Do tych przeglÄ…darek oczywiÅ›cie nie zaliczka siÄ™ Internet Explorer ze swoimi genialnymi pomysÅ‚ami :-) W IE nie skorzystamy z obiektu XMLHttpRequest, zamiast tego mamy do dyspozycji XDomainRequest, z którym możemy nawiÄ…zywać połączenia poza domenÄ™.

Czyli tak wygląda obsługa CORS dla Internet Explorer:

1
2
3
if (!'withCredentials' in new XMLHttpRequest() && XDomainRequest) {
    // CORS supported (XHR)
}

Złączając powyższe techniki otrzymujemy następujący kod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var url = 'http://other.domain.com/';
if (XMLHttpRequest) {
    var request = new XMLHttpRequest();
    if ('withCredentials' in request) {
        request.open('GET', url, true);
        request.onreadystatechange = handler;
        request.send();
    } else if (XDomainRequest) {
        var xdr = new XDomainRequest();
            xdr.open('get', url);
            xdr.send();
    } else {
        // CORS not supported
    }
}

Dobra informacja dla korzystajÄ…cych z jQuery: jest już skrypt Å‚atajÄ…cy uÅ‚omnoÅ›ci Internet Explorer – iecors.js. DziÄ™ki niemu możemy wykonywać żądania XHR poza domenÄ™ nie martwiÄ…c siÄ™ dodatkowÄ… obsÅ‚ugÄ… IE/XDR (choć dalej musimy siÄ™ martwić o przeglÄ…darki nie obsÅ‚ugujÄ…ce CORS).

Cross Browser Polyfills

Jak to bywa z nowymi technologiami, rzadko kiedy wszystkie przeglądarki je obsługują. Także i CORS nie jest wspierany przez wszystkie przeglądarki, w tym lubianą przeze mnie Operę. Rozwiązań problemu mamy kilka:

  • Użyć CORS polyfill
    Najprostszym sposobem na brak natywnego wsparcia ze strony przeglÄ…darki jest zastosowanie biblioteki, która dodaje owe funkcjonalnoÅ›ci. W tym celu możemy skorzystać z pmxdr (wykorzystuje innÄ… technologiÄ™ – postMessage, który również nie wszÄ™dzie jest obsÅ‚ugiwana :)) czy flXHR (wykorzystuje brzydkiego Flasha).
  • Użyć JSONP, iframe lub innÄ… technikÄ™ do Cross-Domain JavaScript
  • WyÅ›wietlić informacjÄ™ o błędzie
    Możemy próbować wymusić na użytkowniku konieczność zmiany lub aktualizacji przeglądarki, co rzadko kiedy jest dobrym wyjściem z sytuacji :-)
Konfiguracja serwera dla żądań XHR

Kolejnym wymogiem do obsÅ‚ugi CORS jest odpowiednie skonfigurowanie serwera, do którego odwoÅ‚ujemy siÄ™ w naszych żądaniach XHR. Czyli mówiÄ…c inaczej – nie możemy odwoÅ‚ywać siÄ™ do wszystkich domen w Sieci :-) Administrator serwera / wÅ‚aÅ›ciciel strony musi jasno okreÅ›lić, że z jego strony mogÄ… być pobierane treÅ›ci przez XHR.

Aby móc pobierać treści z zewnętrznych serwerów, muszą one w odpowiedzi wysyłać nagłówek Access-Control-Allow-Origin wskazujący na naszą domenę:

1
Header set Access-Control-Allow-Origin http://moja-domena.pl/

lub dowolnÄ… domenÄ™:

1
Header set Access-Control-Allow-Origin *

Powyższe ustawienia dotyczą oczywiście serwera Apache. Analogicznie, ten sam efekt możemy osiągnąć z PHP:

1
header("Access-Control-Allow-Origin: *");

czy serwerem IIS7:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<configuration>
 <system.webServer>
   <httpProtocol>
     <customHeaders>
       <add name="Access-Control-Allow-Origin" value="*" />
     </customHeaders>
   </httpProtocol>
 </system.webServer>
</configuration>
Credentialed requests

Nagłówek Access-Control-Allow-Credentials pozwala określić czy wraz z odpowiedzią mają być wysyłane także cookies w nagłówku. Aby z kolei określić czy wraz z żądaniem mają być przesyłane ciasteczka, musimy nasz obiekt XMLHttpRequest uzupełnić o pole withCredentials:

1
2
3
4
5
6
7
8
9
10
var url = "http://other.domain.com/";
if (XMLHttpRequest) {
    var request = new XMLHttpRequest();
    if ('withCredentials' in request) {
        request.open('GET', url, true);
        request.withCredentials = 'true';
        request.onreadystatechange = handler;
        request.send();
    }
}

Niestety obiekt XDomainRequest nie obsługuje możliwości przesyłania ciasteczek między domenami.

Przykład działania: Simple use of Cross-Site XMLHttpRequest (with Credentials) (można testować w Chrome lub Firefoxie).

Preflight requests

Preflight requests to dość nietypowy sposób komunikacji dla żądaÅ„ wykonywanych przy pomocy XHR (i tylko XHR, bowiem obiekt XDomainRequest tego nie obsÅ‚uguje). W najwiÄ™kszym skrócie – żądania z niestandardowymi nagłówkami (nie znajdujÄ…cymi siÄ™ w specyfikacji HTTP 1.1) lub o niestandardowym typie MIME (innym niż text/plain, multipart/form-data lub application/x-www-form-urlencoded) to żądania preflighted (nie znam polskiego odpowiednika tego sÅ‚owa).

Czym różni się preflight requests od zwykłego żądania? Najłatwiej będzie to przedstawić na przykładzie:

  1. WysyÅ‚amy żądanie XHR do obcego serwera – zapytanie zawiera niestandardowy nagłówek App-Token oraz niestandardowy typ MIME – application/xml (choć wystarczyÅ‚o speÅ‚nić jeden warunek, by żądanie byÅ‚o preflighted):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var request =   new XMLHttpRequest(),
        post    =   ['<?xml version="1.0"?>',
                        '<post>',
                            '<title>TytuÅ‚</title>',
                            '<body>Treść</body>',
                        '</post>'
                    ].join('');

    if (request) {
        request.open('GET', 'http://my-app.pl/post.php', true);
        request.setRequestHeader('App-Token', '^#$%Fgvgdf%^#$&%^TGbV');
        request.setRequestHeader('Content-Type', 'application/xml');
        request.onreadystatechange = handler;
        request.send(post);
    }
  2. Teraz nastÄ™puje nietypowe zjawisko – nasz serwer wysyÅ‚a zapytanie HTTP (Request) z użyciem metody OPTIONS do pożądanego adresu URL, w tym przypadku http://my-app.pl/post.php. Tak może wyglÄ…dać owe zapytanie (pominÄ…Å‚em tutaj nieinteresujÄ…ce nas nagłówki):
    1
    2
    3
    4
    5
    OPTIONS /public/ HTTP/1.1
    Host: my-app.pl
    Origin: http://blog.kamilbrenk.pl/
    Access-Control-Request-Method: POST  
    Access-Control-Request-Headers: App-Token

    Jak widać, w naszym zapytaniu mamy nietypowe nagłówki:

    • Access-Control-Request-Method – metoda, której chcemy użyć żądaniu,
    • Access-Control-Request-Headers – lista niestandardowych nagłówków, które chcemy użyć (opcjonalnie).
  3. Dostajemy odpowiedź od serwera:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    HTTP/1.1 200 OK  
    Date: Mon, 12 Oct 2011 01:15:39 GMT
    Server: Apache/2.0.61 (Unix)
    Access-Control-Allow-Origin: http://blog.kamilbrenk.pl/
    Access-Control-Allow-Methods: POST, GET, OPTIONS
    Access-Control-Allow-Headers: App-Token
    Access-Control-Max-Age: 1728000
    Content-Length: 0
    Content-Type: text/plain

    Serwer przesyła kilka interesujących nas nagłówków, w tym:

    • Access-Control-Allow-Origin – informuje, że z podanej domeny można wykonać żądanie XHR,
    • Access-Control-Allow-Methods – dozwolone metody przesyÅ‚u, w tym POST, której użyliÅ›my w naszym żądaniu,
    • Access-Control-Allow-Headers – lista niestandardowych nagłówków, których możemy użyć,
    • Access-Control-Max-Age – czas trzymania w cache informacji o „rzetelnoÅ›ci” wykonywanego żądania; oznacza to, że przez 1 728 000 sekund (20 dni) serwer nie bÄ™dzie wykonywaÅ‚ preflight requests – każde dodatkowe żądanie do dodatkowe bajty, wiÄ™c buforowanie wyniku to zwykÅ‚a optymalizacja.
  4. Serwer, do którego wysyłamy nasze żądanie POST zgodził się na odbiór przesyłki. Czas więc na właściwe żądanie:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    POST /public/ HTTP/1.1
    Host: my-app.pl
    App-Token: ^#$%Fgvgdf%^#$&%^TGbV
    Content-Type: application/xml; charset=UTF-8
    Content-Length: 72
    Origin: http://blog.kamilbrenk.pl/
    Pragma: no-cache
    Cache-Control: no-cache

    <?xml version="1.0"?><post><title>Tytuł</title><body>Treść</body></post>
  5. Odpowiedź jest już standardowa i nie powinna być zaskoczeniem:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    HTTP/1.1 200 OK
    Date: Mon, 12 Oct 2011 01:15:40 GMT
    Server: Apache/2.0.61 (Unix)
    Access-Control-Allow-Origin: http://blog.kamilbrenk.pl/
    Content-Encoding: gzip
    Content-Length: 138
    Content-Type: text/plain

    [Some GZIP'd payload]

Mam nadzieję, że powyższy przykład nie jest zbyt zagmatwany i wszystko wyjaśnia :) Preflight requests mają za zadanie sprawdzenie czy wysłanie naszego nietypowego żądania ma jakikolwiek sens oraz zwiększa bezpieczeństwo (sprawdza czy serwer, do którego wysyłamy obsłuży nasze zapytanie).

Kilka słów podsumowania

Jak już zauważyÅ‚eÅ›, wykorzystywanie CORS jest banalnie proste – wystarczy nowoczesna przeglÄ…darka obsÅ‚ugujÄ…ca obiekt XMLHttpRequest2 oraz odpowiednia konfiguracja serwera, ewentualnie wsparcie dla IE (XDR), co zaÅ‚atwia wspomniana biblioteka do jQuery.

Gorzej jeÅ›li zależy nam także na użytkownikach Opery – wtedy musimy użyć JSONP lub innej techniki. Doskonale sprawdzi siÄ™ także plugin Cross-Domain Ajax mod omawiany tutaj.

Na sam koniec warto też wspomnieć o bezpieczeÅ„stwie – „otworzenie” serwisu na zewnÄ™trzne żądania XHR może mieć swoje negatywne skutki i zostać wykorzystane w nieczystych celach :-) Warto wiÄ™c poznać niebezpieczeÅ„stwa zwiÄ…zane z Cross Origin Request.

Komentarze (5)

  1. Piotrek Reinmar Koszuliński 10 października 2011

    O! Mogę wykreślić jeden temat z http://code42.pl/2011/08/05/javascript-po-polsku/ Dobra robota :)

  2. Piotrek Reinmar Koszuliński 10 października 2011

    Hym… Chociaż brakuje trochÄ™ informacji o preflight requests :). To dziwaczna i zaskakujÄ…ca konstrukcja, którÄ… warto opisać w tym temacie. Sam straciÅ‚em z godzinÄ™ nim siÄ™ skapnÄ…Å‚em czemu Fx wysyÅ‚a request z metodÄ… OPTIONS kiedy ja chcÄ™ POST :)

  3. Kamil Brenk 11 października 2011

    Dzięki Piotrek ;) Racja, czytałem w dokumentacji o preflight requests i trochę to zagmatwane :) później uzupełnię wpis, żeby był kompletny.

    Edit: done!

  4. arhiman 29 grudnia 2011

    Świetny opis! Co ciekawe jQuery w wersji > 1.5 (na pewno w wersji 1.7) już nie wysyła nagłówków OPTIONS. W wersji 1.3 biblioteki miałem problem z parami zapytań OPTIONS + POST/GET, a w wersji 1.7 już mam tylko jeden request.
    Dodatkowo wystarczy nagłówek Access-Control-Allow-Origin po stronie serwera, do którego leci żądanie.
    Nie do koÅ„ca rozumiem, dlaczego nie ma teraz nagłówka OPTIONS, ale zapewne nowsza wersja biblioteki inaczej konstruuje zapytania XHR… ;)

  5. Kamil Brenk 30 grudnia 2011

    @arhiman: dzięki za komentarz :)

    A to dziwne co piszesz, bo z tego co widzę dokumentacja CORS się nie zmieniła i przy Preflight requests dalej musimy pytać serwer o możliwość przyjęcia żądania (z wykorzystaniem żądania OPTIONS). W wolnej chwili muszę to przetestować.



Kamil Brenk Blog

PHP, JavaScript, SQL, HTML

  • Informacje o blogu

    Kamil Brenk

    Blog o tworzeniu aplikacji na potrzeby sieci Web.

    Praktyczne przykłady, porady i sztuczki. PHP, SQL, AJAX, JavaScript, HTML i pochodne.

    Kanał RSS

    • Najnowsze
    • Komentarze
    • Popularne
    • Liczniki w CSS
    • Wyprzedaż książek o programowaniu!
    • Niestandardowy placeholder
    • JavaScript w moduÅ‚ach
    • Co dalej z blogiem?
    • Interaktywna mapa w HTML i CSS
    • Olsztyn: Jak wyseparować zawartość zassanÄ… przez file_get_content?
    • ERMLAB: Od czegoÅ› trzeba zacząć :) Wiele osób wÅ‚aÅ›nie stawia na...
    • david: co nalezy wkleić na stronÄ™ aby plik ze stylami byÅ‚ ladowany...
    • krynicz: Nie jestem pewien czy dobrze to rozumiem: wpisujemy OG w...
    • yaro: Jak zmienić re_write znak "_" na "-"?
    • Piotr: stworzyÅ‚em prostÄ… stronkÄ™ w PHP, czy jest możliwość aby...
    • MichalR: Super sprawa... bardzo przydatne.. dzieki i pozdrawiam..
    • Niestandardowe czcionki na stronie
    • Sposoby wczytywania JavaScript
    • Gramatyka w PHP, część 1
    • Umowa i zaliczka dla freelancera
    • WysyÅ‚anie wiadomoÅ›ci SMS w PHP
    • Projekt aplikacji po stronie klienta
    • WÅ‚asny mechanizm Feed
  • Szukajka
    Wpisz co chcesz wyszukać na stronie…
  • Kategorie
    • Apache
    • Freelancer
    • Front-end Development
    • HTML5 & CSS3
    • Inne
    • JavaScript
    • Książki
    • PHP
    • Po godzinach
    • Pozycjonowanie
    • Protokół HTTP
    • SQL
    • Wyrażenia regularne
  • Moje serwisy
    • Testy zawodowe
    • MiÅ‚ość, uczucia i seks
  • Czytane blogi
    • Wojciech Sznapka
    • Wojciech SoczyÅ„ski
    • MichaÅ‚ Wachowski
    • Tomasz Kowalczyk
    • Filip GórczyÅ„ski
  • Strona główna
  • Curriculum Vitae
  • O mnie
  • PrzykÅ‚ad: Gramatyka w PHP
  • PrzykÅ‚ad: Kompresja CSS
  • PrzykÅ‚ad: Kompresja JavaScript
  • PrzykÅ‚ad: Skracanie linków
  • PrzykÅ‚ad: Wykrywanie serwera HTTP
  • PrzykÅ‚ad: WÅ‚asna bramka SMS
  • Mapa strony
  • Kontakt

Kamil Brenk © 2010. All rights reserved.

Designed by FTL Wordpress Themes brought to you by Smashing Magazine.

Do góry ∧