Jak ominąć same-origin policy

Przeglądarki nie pozwalają na AJAXa poza własną domeną. Czasem jednak mamy API w innej domenie i po prostu musimy z niego skorzystać. Zamiast stawiać proxy możemy użyć jednej z dwóch dostępnych metod – JSONP lub CORS. Dziś wyjaśnimy sobie na czym polegają.

JSONP

Wygląda, jakby ktoś chciał dodać emota do słowa JSON ale zabrakło mu dwukropka. :P Litera P oznacza Padding. W praktyce JSONP jest ekstremalnie prosty.

Tworzymy po prostu tag <script>, w którym podajemy zewnętrzny adres. Przeglądarki nie stosują same-origin policy do tagu <script>. Dzięki temu możemy kierować gdzie chcemy.

<script src="http://my.api/json_data.php"></script>

Ale zaraz, przecież tak zwrócony json nie jest kodem JavaScript. Nic się nie wykona. Nie dostaniemy danych. Przeglądarka zgłosi błąd.

Rozwiązanie jest prostsze niż myślimy. Lekko zmieniamy adres dodając nazwę funkcji:

<script src="http://my.api/json_data.php?jsonp=parse"></script>

Co to zmienia? Otóż serwer może zwrócić nam dane opakowane w funkcję. Odpowiedź serwera będzie wyglądać np. tak:

parse({
   id: 1,
   title: "Nasze dane w json",
   content: "..."
});

W kodzie JavaScript będziemy mieć przygotowaną funkcję parse(). Kiedy skrypt się załaduje otrzyma ona dane. Może wyglądać np. tak:

function parse(json) {
  console.log(json);
}

Prawda, że łatwo? W ten sposób możemy pobrać dowolne dane JSON ze zdalnego serwera.

W AngularJS mamy jeszcze łatwiej. Framework daje nam specjalną metodę do wywołań JSONP. Wystarczy tylko w URL podać specjalną Angularową nazwę funkcji JSON_CALLBACK a reszta działa tak, jak każdy inny AJAX:

var url = "http://my.api/json_data.php?jsonp=JSON_CALLBACK";
   $http.jsonp(url).success(function(data){
   console.log(data);
});

Oczywiście tą drogą nie możemy wybrać sobie metody HTTP ani ustawić nagłówków. Całość robi za nas przeglądarka.

Cross-Origin Resource Sharing

Druga metoda omijania SOP polega na odpowiednim ustawianiu nagłówków HTTP. Przeglądarka wysyła do serwera nagłówek „Origin”. Mówi w nim z jakiej strony chcemy się dostać do zasobu. Serwer musi zgodzić się na stronę źródłową. Załóżmy, że strona jest pod adresem http://moja-strona.pl a API ma adres http://moje-api.pl/api. Żądanie przeglądarki będzie wyglądać tak:

GET /api HTTP/1.1
Origin: http://moja-strona.pl
Host: moje-api.pl
Accept-Language: en-US
User-Agent: Mozilla/5.0
...

Serwer odpowie np. takimi nagłówkami:

Access-Control-Allow-Origin: http://moja-strona.pl
Content-Type: text/html; charset=utf-8

Nagłówek Access-Control-Allow-Origin mówi przeglądarce, że serwer zgadza się, żeby moja-strona.pl sięgała do naszego API. Można w tym miejscu ustawić gwiazdkę, wtedy serwer zgadza się na wszystkie strony.

To prawie cała filozofia CORS. Przeglądarka pyta się serwera o zgodę. Serwer zgadza się lub nie. Ważne, że to nie my kontrolujemy nagłówek Origin ale przeglądarka. Z poziomu JavaScriptu nie możemy namieszać i udawać, że jesteśmy inną stroną.

Większość zabawy odbywa się po stronie serwera. Napiszmy go więc najpierw. Nasz przykładowy plik index.php będzie zwracał w JSON informacje o otrzymanych nagłówkach:

// akceptujemy stronę, z której przychodzi żądanie
if (isset($_SERVER['HTTP_ORIGIN'])) {
   header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
}

switch ($_SERVER['REQUEST_METHOD']) {

   // options, trzeba podać akceptowane komendy i nagłówki
   case 'OPTIONS':
      if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']))
         header("Access-Control-Allow-Methods: FIRE, OPTIONS");
      if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']))
         header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}");
      break;

   // metoda fire, zwracamy jakieś dane
   case 'FIRE':
      header("Content-type: application/json");
      echo json_encode(apache_request_headers());
      break;

   // nie zezwalamy na inne metody. 405 - method not allowed
   default:
      http_response_code(405);
      break;
}

Jak widać serwer obsługuje metody OPTIONS i naszą ulubioną metodę odpalającą przykład czyli FIRE. Jeśli przyjdzie OPTIONS, odpowiadamy co akceptujemy (metody i nagłówki). Jeśli przyjdzie FIRE, zwracamy jsonem nagłówki zapytania (zachowujemy się trochę jak metoda TRACE). Gdy przeglądarka wykona inną metodę odpowiadamy kodem 405 – metoda niedozwolona.

Nasz serwer jest trochę zbyt pobłażliwy. Nie powinien akceptować wszystkich HTTP_ORIGIN. W normalnej aplikacji powinniśmy tutaj skorzystać z jakiejś bazy stron, które akceptujemy.

Po stronie czystego JavaScript sprawa trochę się komplikuje. Problemów jest kilka:

  1. Przeglądarka musi obsługiwać CORS. Musi więc mieć obiekt XmlHttpRequest2 (nowsza wersja). W IE zamiast niego jest obiekt XDomainRequest.
  2. Przed wysłaniem żądania przeglądarka przeważnie pyta się serwera o zgodę metodą OPTIONS. Musimy ze strony serwera obsłużyć takie zapytanie.
  3. Ciastka są wysyłane tylko, gdy wprost zażądamy ich wysłania a serwer się zgodzi.

Dlatego przeskoczymy od razu do frameworków. W jQuery lub AngularJS jest o wiele łatwiej. Frameworki za nas zajmują się ustawieniem nagłówków, znalezieniem obiektu itp. Właściwie cała robota jest po ich stronie. Nasze wywołanie AJAXa nie różni się od każdego innego. Framework z przeglądarką sam domyśla się o co chodzi.

Załóżmy, że nasze api jest pod adresem http://moje-api.pl/api/index.php. Oczywiście sam kod JavaScript musi być gdzie indziej żeby pokazać siłę CORS.

W jQuery robimy zwykłe zapytanie AJAX:

$.ajax({
   type: 'FIRE',
   url: 'http://moje-api.pl/api/index.php',
   contentType: 'application/json',
   success: function(data,status,jqXHR) {
      console.log(data);
   },
   error: function() {
      alert('CORS error');
   }
});

Podobnie w AngularJS. Nasze wywołanie niczym nie różni się od normalnego (pomijam aplikację, kontrolery itp). Framework sam rozpozna, że robimy CORS:

$http({
   method: 'FIRE',
   url: 'http://moje-api.pl/api/index.php'
}).success(function(data,status,headers,config){
   console.log(data);
});

To prawie wszystko. Mamy do wyboru jeszcze kilka przydatnych opcji.

Ciasteczka w CORS

Przesyłanie ciastek włączamy w obiekcie XHR ustawiając jego atrybut withCredentials. Serwer musi wtedy odpowiedzieć odpowiednim nagłówkiem:

Access-Control-Allow-Credentials: true

W jQuery ustawiamy to w parametrach wywołania AJAXa:

$.ajax({
   type: 'FIRE',
   url: 'http://moje-api.pl/api/index.php',
   contentType: 'application/json',

   xhrFields: {
      withCredentials: true
   },

   // ...
});

W AngularJS mamy odpowiednią opcję już w samej konfiguracji:

$http({
   method: 'FIRE',
   url: 'http://moje-api.pl/api/index.php',
   withCredentials: true
}).success(function(data,status,headers,config){
   console.log(data);
});

Uwaga! Ciasteczka też działają zgodnie z same-origin policy. Jeśli więc wywołujemy zdalne API to przeglądarka wyśle i ustawi ciasteczka dla adresu tego API. Nie zobaczymy ich w naszym JavaScripcie.

Na koniec

To tylko próbka informacji o CORS. Czasem przeglądarki nie przesyłają OPTIONS. Czasem udają, że obsługują CORS choć tak naprawdę tego nie robią. Jest jeszcze kilka ciekawych nagłówków do ustawienia. Więcej informacji znajdziemy w artykule o CORS.

Kontrola serwera daje nam rozsądny poziom bezpieczeństwa. Nie najwyższy, ale też nie jest to całkowity brak zabezpieczeń. CORS jest obecnie standardem. Bez niego nie moglibyśmy budować złożonych systemów z wielu API na wielu serwerach.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *