CORS を前提としたコードを書くときのポイント
- AngularJS では $http のヘッダーに ‘Content-Type’: ‘application/x-www-form-urlencoded’ を追加する。しかし、Basic 認証をはじめ、カスタムヘッダーを追加する場合、この Content-Type 追加は意味が無い。
- サーバでは、プリフライトリクエストがあった場合に対応する次のヘッダーをレスポンスに含めるようにする。
- Access-Control-Allow-Origin:* ← 許可する接続元(* はどこからでも受け入れる)
- Access-Control-Allow-Methods ← 許可するメソッド(POST など)
- Access-Control-Allow-Headers ← 許可するヘッダー(authorization や独自のカスタムヘッダーなど)
CORS = HTTP access control = クロスサイト HTTP アクセス制限、とは?
- ブラウザは、アクセスしたページ以外のドメインから何かを取ってくることを制限している。基本的にはできないようになっている。
- そのため、たとえば次のようなことができない。ローカルホスト上のウェブサーバに AngularJS のサンプルを置いたとする。ページのボタンを押したら、とあるサイトからデータを取ってきて表示する。
- ローカルホストドメインと、とあるサイトのドメインは異なる。ウェブブラウザでローカルホストのページを表示している。そのため、別ドメインであるとあるサイトのデータへのアクセスは制限される。
- これは、セキュリティ上の観点から施された、ブラウザの仕様
リクエストを送信するウェブブラウザから見た CORS 時に必要な措置
- 実は特に無い。リクエストする内容が受け入れ可能かどうか、サーバ側で判断するだけなため。
- サーバの審査が不要(プリフライトリクエストが発生しない)なリクエストの種類はある。次の通り。
- メソッドが GET である場合はウェブブラウザはプリフライトリクエストを送信しない。
- メソッドが POST で Content-Type が application/x-www-form-urlencoded の場合はウェブブラウザはプリフライトリクエストを送信しない。
- メソッドが POST で Content-Type が multipart/form-data の場合はウェブブラウザはプリフライトリクエストを送信しない。
- メソッドが POST で Content-Type が text/plain の場合ははウェブブラウザはプリフライトリクエストを送信しない。
- リクエストにカスタムヘッダーを追加しない場合。(Basic 認証の Authorization はカスタムヘッダー扱い)
- プリフライトリクエストを発生させないために AngularJS の $http に追加するのは次の設定
- ‘Content-Type’: ‘application/x-www-form-urlencoded’
- ただしカスタムヘッダーを追加する場合は、Basic 認証のヘッダーを追加する場合は意味なし。
参考ページですの♪
リクエストを受信するサーバから見た CORS 時に必要な措置
- cookie や認証が無ければ、サーバは Access-Control-Allow-Origin: * ヘッダーを返すだけで良い。PHP の場合は、「header(‘Access-Control-Allow-Origin: *’);」
- プリフライトリクエストはブラウザから次を送ろうとすると発生し、サーバへ送信される。
- GET ではないリクエストの場合にプリフライトリクエストが発生
- POST でも Content-Type が application/x-www-form-urlencoded ではないリクエストの場合にプリフライトリクエストが発生
- POST でも Content-Type が multipart/form-data ではないリクエストの場合にプリフライトリクエストが発生
- POST でも Content-Type が text/plain ではないリクエストの場合にプリフライトリクエストが発生
- カスタムヘッダーが付いている場合にプリフライトリクエストが発生
- プリフライトリクエストのメソッドは OPTIONとなる。
- プリフライトリクエストへのレスポンスに必要なヘッダーは次の2つ。(この情報にたどり着くまでに苦労した!)
- Access-Control-Allow-Method
PHP だとたとえば「header(‘Access-Control-Allow-Methods: POST’);」 - Access-Control-Allow-Headers
PHP だとたとえば「header(‘Access-Control-Allow-Headers: accept, content-type, authorization’);」
- Access-Control-Allow-Method
なお、Credentialed requests というのもあるそうですけれども、ここでは割愛いたします。
参考ページですの♪
サンプルコード
- ウェブブラウザからプリフライトリクエストをサーバへ送信
- Request Method: OPTIONS
← プリフライトリクエスの場合はメソッドが OPTIONS - Access-Control-Request-Method:POST
← 通常リクエスト時のメソッドは何かを設定 - Access-Control-Request-Headers:accept, authorization, content-type
← 通常リクエスト時のヘッダーは何かを設定 - メソッドは POST で Content-Type が application/x-www-form-urlencoded のため、また、カスタムヘッダーも存在しないため一見するとプリフライトリクエストが発生しないように思える。
しかし、Basic 認証に使う Authorization がカスタムヘッダー扱いとみなされていると思われるが、プリフライトリクエストが発生した。
- Request Method: OPTIONS
- サーバから「リクエストを受け入れ可能か否かの内容」のレスポンスが送信され、ウェブブラウザはこれを受信。
今回の例では、次の内容なら受け入れ可能とレスポンスするようにした。- Access-Control-Allow-Origin:*
- Access-Control-Allow-Methods:POST
- Access-Control-Allow-Headers:accept, content-type, authorization
- 「サーバーによるリクエスト受け入れが可能という内容のレスポンス」であった場合、ウェブブラウザから通常のリクエストをサーバへ送信
- サーバからレスポンスが送信され、ウェブブラウザはこれを受信
index.html
<!doctype html> <html ng-app="httpApp"> <head> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.min.js"></script> <script> var httpApp = angular.module('httpApp',[]); httpApp.controller('SendController', ['$scope', '$http', function($scope, $http) { $scope.dog = { name: "Fido", dob: new Date(), legs: [1, 2, 3, 4] }; $scope.send = function() { var url = "https://oki2a24.com/test/json.php", config = { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Basic YmFzaWN1c2VyOmJhc2ljcGFzcw==' }, timeout: 5000 }; $http.post(url, $scope.dog, config).then(function(response) { $scope.data = response.data; $scope.status = response.status; }).catch(function(response) { $scope.data = response.data || "Request failed"; $scope.status = response.status; }); } }]); </script> </head> <body> <div ng-controller="SendController"> <h2>送信データ</h2> {{dog}} <ul> <li>{{dog.name}}</li> <li>{{dog.dob}}</li> <li>{{dog.legs}}</li> </ul> <h2>送信</h2> <button ng-click="send()">送信</button> <h2>結果</h2> <ul> <li>{{data}}</li> <li>{{status}}</li> </ul> </div> </body> </html>
json.php
前回からの違いです。↓
- Access-Control-Allow-Methods、Access-Control-Allow-Headers を追加
- ウェブサーバ(Nginx)ではなく、PHP で Basic 認証を行うように処理を追加
<?php header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: POST'); header('Access-Control-Allow-Headers: accept, content-type, authorization'); // ウェブサーバではなく、PHP で Basic 認証を行う。 switch (true) { case !isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']): case $_SERVER['PHP_AUTH_USER'] !== 'basicuser': case $_SERVER['PHP_AUTH_PW'] !== 'basicpass': // 次のヘッダーがある場合、 // preflight リクエストの次の実際のリクエストが飛ばないため // コメントとする。 // この場合、認証機能は正常に働くが、 // 認証が通らない時のレスポンスを 401 に設定することができない。 //header('WWW-Authenticate: Basic realm="Enter username and password."'); //header('HTTP/1.0 401 Unauthorized'); header('Content-Type: text/plain; charset=utf-8'); die('このページを見るにはログインが必要です'); } $json_string = file_get_contents('php://input'); ob_start(); var_dump(json_decode($json_string)); $out = ob_get_contents(); ob_end_clean(); file_put_contents('recieved_json.txt', $out); // レスポンス header 関数無くてもレスポンスできたが、Volley だからなのかどうかは不明 header("HTTP/1.1 200 OK"); header("Status: 200"); header("Content-Type: application/json; charset=utf-8"); header('X-Content-Type-Options: nosniff'); $response = array('response' => 'OK'); echo json_encode($response);
サンプルコードの実行結果
プリフライトリクエストと通常のリクエストが連続してサーバに送信されました。
「連続して」というのは、ウェブページの「送信」を1回クリックしたらプリフライトリクエストが、次に通常のリクエストが自動的に連続して送信された、ということを意味しております。
プリフライトリクエスト
▼General Remote Address:27.120.110.175:80 Request URL:https://oki2a24.com/test/json.php Request Method:OPTIONS Status Code:200 OK ▼Response Headers HTTP/1.1 200 OK Server: nginx Date: Fri, 28 Aug 2015 14:49:48 GMT Content-Type: text/plain; charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive Vary: Accept-Encoding Access-Control-Allow-Origin: * Access-Control-Allow-Methods: POST Access-Control-Allow-Headers: accept, content-type, authorization Content-Encoding: gzip ▼Request Headers OPTIONS /test/json.php HTTP/1.1 Host: oki2a24.com Connection: keep-alive Access-Control-Request-Method: POST Origin: http://localhost:3000 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36 Access-Control-Request-Headers: accept, authorization, content-type Accept: */* Referer: http://localhost:3000/ Accept-Encoding: gzip, deflate, sdch Accept-Language: ja,en-US;q=0.8,en;q=0.6
通常のリクエスト
▼General Remote Address:27.120.110.175:80 Request URL:https://oki2a24.com/test/json.php Request Method:POST Status Code:200 OK ▼Response Headers HTTP/1.1 200 OK Server: nginx Date: Fri, 28 Aug 2015 14:49:48 GMT Content-Type: application/json; charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive Vary: Accept-Encoding Access-Control-Allow-Origin: * Access-Control-Allow-Methods: POST Access-Control-Allow-Headers: accept, content-type, authorization X-Content-Type-Options: nosniff Content-Encoding: gzip ▼Request Headers POST /test/json.php HTTP/1.1 Host: oki2a24.com Connection: keep-alive Content-Length: 65 Accept: application/json, text/plain, */* Origin: http://localhost:3000 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36 Authorization: Basic YmFzaWN1c2VyOmJhc2ljcGFzcw== Content-Type: application/x-www-form-urlencoded Referer: http://localhost:3000/ Accept-Encoding: gzip, deflate Accept-Language: ja,en-US;q=0.8,en;q=0.6 ▼Form Data {"name":"Fido","dob":"2015-08-28T14:49:47.011Z","legs":[1,2,3,4]}
おわりに
いつもどおり、整理がついてしまえばたいして難しくないお話でしたの♪
意味がわからない状態を突破できたのは、次のページに出会うことができたからでした!AngularJS と PHP との間のリクエスト・レスポンスの具体的なやりとりをイメージすることができました。ありがとう存じます!
一度理解をしてしまうと、こちらのページの内容もすっと頭に入ってきました。実は調べ始めてから最初の方に読んでいたのですけれども、そのときは理解できていませんでしたの><。
わかってしまえば、さらに!、英語で書かれていてもサーバ側の動きもずいぶんと理解することができました。
また、PHP で Basic 認証をする方法や、Basic 認証については次のページが参考になりました。
- PHPを利用したBasic認証の仕組み – Qiita
- PHP: PHP による HTTP 認証 – Manual
- RFC 7235 – HTTP/1.1: Authentication (日本語訳)
前回のできません><!の投稿からどれくらい経ったかしら。。。1ヶ月くらいですわね。
ようやく解決できましたの♪勉強は、楽しいですわ♪最後に、今までの試行錯誤の投稿の目次を作っておわりとしたいと思います。
- 【Nginx】Basic 認証をかける方法手順メモ | oki2a24
- cURL を使って Basic 認証が必要な PHP へ JSON データを POST する手順メモ | oki2a24
- AngularJS を使って PHP へ JSON データを POST する方法 | oki2a24
- AngularJS を使って Basic 認証が必要な PHP へ JSON データを POST できません>< | oki2a24
- AngularJS を使って Basic 認証が必要な PHP へ JSON データを POST するサンプルコード! | oki2a24 ← 本投稿
以上です。