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:
- 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
15var 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);
} - 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
5OPTIONS /public/ HTTP/1.1
Host: my-app.pl
Origin: http://blog.kamilbrenk.pl/
Access-Control-Request-Method: POST
Access-Control-Request-Headers: App-TokenJak 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).
- Dostajemy odpowiedź od serwera:
1
2
3
4
5
6
7
8
9HTTP/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/plainSerwer 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.
- 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
10POST /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> - Odpowiedź jest już standardowa i nie powinna być zaskoczeniem:
1
2
3
4
5
6
7
8
9HTTP/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.
O! Mogę wykreślić jeden temat z http://code42.pl/2011/08/05/javascript-po-polsku/ Dobra robota :)
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 :)
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!
Ś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… ;)
@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ć.