Angular2のクロスドメインの対応を備忘録としてまとめました。私がAngular2のSPAを開発する時、フロントエンド側がangular-cliで、バックエンド(WEB-API)側がPHP+IISとなる組み合わせが多く、フロントエンドとバックエンドで開発用WEBサーバが異なるため(ポートも異なるため)、どうしてもクロスドメイン環境になってしまいます。この開発環境下において、IE11は特に問題が発生しませんでしたが、FireFox52.0.2とChrome57.0.2987.133ではクロスドメインのアクセスの問題が発生しました。
クロスドメインアクセスの問題とは
ブラウザに実装されている”同一生成元ポリシー”の制約により、自分を生成したドメイン以外のドメインのサーバと通信することができない事が問題としてあげられます。中規模以下のシステムの場合、通常、フロントエンド側のSPAのプログラム(HTML、JavaScript、CSS)とバックエンド側のWEB-APIのプログラム(PHP、Java、,NETなどのアプリケーションサーバとして構築)は同じドメイン配下のサーバに配置される事が多いと思いますので、クロスドメインの問題は発生しません。しかし、この両者のプログラムが異なるドメインのサーバに配置された場合、クロスドメインの問題が発生する事になります。
(エラー発生状況)
フロントエンド側開発をangular-cli(簡易WEBサーバ)、バックエンド側開発をPHP(WEBサーバはIIS)で行っている時、下記のようにHTTP通信処理でエラーが発生し、error.text();の戻り値として{ "isTrusted": true }の内容が戻ってくる状況となりました。
1 2 3 4 5 6 7 8 9 10 11 |
//HTTP通信 this._http.post(url, trans_data, options).subscribe( res => { //正常処理 }, error => { //エラー処理 self.ErrorStatus = error.status; self.ErrorText = error.text(); ※{ "isTrusted": true }の内容が戻ってくる } ); |
下図のエラーは後ほど説明しますが、preflightリクエストに対する応答処理が十分でないため発生しているエラーでした。
とりあえず動作させたい対応
セキュリティ、処理効率などは関係なく、とりあえず動作させたい場合は、バックエンド側処理でレスポンスヘッダに以下の項目を設定する事で対応できると思います。
レスポンスヘッダ | 説明 | 設定例 |
Access-Control-Allow-Origin | 許可するリクエスト元のURLを設定 | *, "http://www.ifelse.jp" |
Access-Control-Allow-Method | 許可するメソッドを設定 | *, "GET,PUT,POST,DELETE" |
Access-Control-Allow-Headers | 許可するヘッダ情報を設定 | content-type |
PHPで記述すると以下のようになります。
1 2 3 4 5 |
<?php header("Access-Control-Allow-Origin:*"); header("Access-Control-Allow-Methods:*"); header("Access-Control-Allow-Headers:content-type"); ?> |
対応指針
クロスドメインアクセスの対応方法としては、以下の3つに大別できそうです。当該ページでは、CORSの対応方法について記述します。CORS(Cross-Origin Resource Sharing)とはクロスドメインアクセスを安全に実現する仕様です。
(1)JSONPで対応する方法
JSONP(JSON with padding)とは、JavaScriptを使用してクロスドメインの問題を解決する方法で、HTMLのscriptタグ、JavaScript(関数)、JSONを組み合わせて実現します。HTMLのscriptタグのsrc属性には、別ドメインのURLを指定することができる点を利用して、別ドメインのサーバからデータを取得することを可能にする方法です。同一生成元ポリシーに従わないため、悪意を持った第三者からの攻撃のリスクが高くなる可能性があります。
(2)CORS(そのままリクエストを送る)
通常のAjaxによりHTTPリクエストと同等で、サーバ側でレスポンスヘッダに「Access-Control-Allow-Origin」を設定する事で対応する方法です。次項に説明する『CORS(最初にpreflightリクエストを送ってから、その後、本来の要求リクエストを送る)』場合の最初のpreflightリクエスト動作がない場合と同等になります。
(3)CORS(最初にpreflightリクエストを送ってから、その後、本来の要求リクエストを送る)
前項に説明した『CORS(そのままリクエストを送る)』にpreflightリクエスト動作を追加した内容になります。最初にpreflightリクエスト(OPTIONSメソッド)をサーバに送信し、サーバ側で接続確認を行いブラウザ側に返信します。接続に問題がなければ、再度、ブラウザは本来の要求リクエストを送り、本来処理を行います。サーバ側ではpreflightリクエストの対応処理を行う必要があります。また、前項同様にサーバ側でレスポンスヘッダに「Access-Control-Allow-Origin」を設定する必要があります。
『CORS(そのままリクエストを送る)』方法と『CORS(最初にpreflightリクエストを送ってから、その後、本来の要求リクエストを送る)』方法のどちらの方法を採用するかは、リクエスト条件によってブラウザが決定します。preflightリクエストの送信有無は以下の条件によってブラウザが決定し、条件全てに当てはまらないリクエストの場合に、preflightリクエストが自動で送られます。
- HTTPメソッドがGET, POST, HEADのいずれか
- HTTPヘッダにAccept, Accept-Language, Content-Language, Content-Type以外のフィールドが含まれない
- Content-Typeの値はapplication/x-www-form-urlencoded, multipart/form-data, text/plainのいずれか
以下のようなajaxでよく見かけるコードは、上記の条件により、『CORS(最初にpreflightリクエストを送ってから、その後、本来の要求リクエストを送る)』方法となります。
1 2 3 4 5 6 7 |
$.ajax({ type: "POST", url: this.url, data: JSON.stringify(sendData), contentType: "application/json", dataType: "text" }) |
CORSのどちらの方式になるかはブラウザが決定するため、サーバ側では正しくpreflightリクエストの対応を行わないとクロスドメインアクセスの問題により通信が出来ない状態となります。
CORS(そのままリクエストを送る)
『CORS(そのままリクエストを送る)』方法でクロスドメインアクセスを対応させるためには、ブラウザがpreflightリクエストを行わない条件でプログラムする必要があります。そのため、フロントエンド側の開発では、前項の説明の通り必然的にボディフォーマットをJSONデータでリクエストする事が出来ないことになります。[キー1=値1&キー2=値2&...]というようにキーと値のペアにするx-www-form-urlencoded形式で送付する必要があります。また、サーバ側では、以下のようにレスポンスヘッダに「Access-Control-Allow-Origin」を設定する必要があります。
レスポンスヘッダ | 説明 | 設定例 |
Access-Control-Allow-Origin | 許可するリクエスト元のURLを設定 | *, "http://www.ifelse.jp" |
PHPで記述すると以下のようになります。
1 2 3 |
<?php header("Access-Control-Allow-Origin:*"); ?> |
しかしながら、フロントエンド側の開発でどうしてもJSON形式のデータフォーマットでリクエストしたい要望も発生すると思います。その時は、リクエストヘッダのContent-Typeをx-www-form-urlencodedに設定してJSON形式のデータを「無理やり」リクエストし、サーバ側処理でリクエストヘッダの内容を判断しながらリクエストデータを取り出す処理を行うことで対応するしかないと思われます。
CORS(最初にpreflightリクエストを送ってから、その後、本来の要求リクエストを送る)
『CORS(最初にpreflightリクエストを送ってから、その後、本来の要求リクエストを送る)』方法でクロスドメインアクセスを対応させるためには、ブラウザがpreflightリクエストを送る前提となりますので、サーバ側でpreflightリクエストに対する対応処理が必要になります。preflightリクエストの以下の項目をチェックします。
preflightリクエストヘッダのOrigin
Originは送信元のドメインが設定され、内容が許可されているドメインであるかチェックする必要があります。Originが許可されていないドメインの場合、preflightリクエスト失敗としてアクセス許可しない必要があります。Originの内容はpreflightリクエスト送信時にブラウザによって自動的に付加されます。
preflightリクエストヘッダのAccess-Control-Request-Method
Access-Control-Request-Methodは、preflightリクエスト後の本来要求分のリクエストで使用するメソッドを知らせるための項目です。許可されていないメソッドが定義されている場合は、preflightリクエスト失敗としてアクセス許可しない必要があります。Access-Control-Request-Methodはpreflightリクエスト送信時にブラウザによって自動的に付加されます。
preflightリクエストヘッダのAccess-Control-Request-Headers
Access-Control-Request-Headersは、Access-Control-Request-Method同様にpreflightリクエスト後の本来要求分のリクエストで使用されるヘッダ情報をサーバー側に知らせるための項目です。許可されていないヘッダ情報が定義されている場合は、preflightリクエスト失敗としてアクセス許可しない必要があります。Access-Control-Request-Headersはpreflightリクエスト送信時にブラウザによって自動的に付加されます。
サーバ側では、上記のpreflightリクエストの各項目の許可チェックを行い、結果をレスポンスヘッダに設定してブラウザ側に返信する必要があります。レスポンスヘッダの項目は下表のようになります。
レスポンスヘッダ | 説明 | 設定例 |
Access-Control-Allow-Origin | 許可するリクエスト元のURLを設定 | *, "http://www.ifelse.jp" |
Access-Control-Allow-Method | 許可するメソッドを設定 | *, "GET,PUT,POST,DELETE" |
Access-Control-Allow-Headers | 許可するヘッダ情報を設定 | content-type |
以下はPHPでpreflightリクエスト処理を記述した例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php //メソッドがOPTIONSの時は、preflightリクエストとして扱う if($_SERVER["REQUEST_METHOD"] == "OPTIONS") { //クロスドメインをチェックする if($_SERVER["HTTP_ORIGIN"] == "http://localhost:4200") { header("Content-Length: 0"); header("Content-Type: text/plain"); header("Access-Control-Allow-Origin:*"); header("Access-Control-Allow-Methods:GET,PUT,POST,DELETE"); header("Access-Control-Allow-Headers:content-type"); } else { header("HTTP/1.1 403 Access Forbidden"); header("Content-Type: text/plain"); echo "You cannot repeat this request"; } } ?> |
以下はSPRING MVCでpreflightリクエスト処理を記述した例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.filter.OncePerRequestFilter; public class CorsFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (request.getHeader("Access-Control-Request-Method") != null && "OPTIONS".equals(request.getMethod())) { if("http://localhost:4200".equals(request.getHeader("Origin"))) { response.addHeader("Access-Control-Allow-Origin", "*"); response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"); response.addHeader("Access-Control-Allow-Headers", "Content-Type"); } else { response.setStatus(HttpServletResponse.SC_FORBIDDEN); } } else { filterChain.doFilter(request, response); } } } |
ブラウザはpreflightリクエスト動作でクロスドメインのサーバがアクセス許可であると認識すると、次に本来要求分のリクエストを送信します。サーバ側では本来要求分のレスポンスヘッダにAccess-Control-Allow-Originの設定を行う必要があります。今までの流れをキャプチャした実際の様子が下図のようになります。
(1)preflightリクエスト動作
上図はpreflightリクエストヘッダとレスポンスヘッダです。preflightリクエストがOPTIONSメソッドでリクエストされている事が分かります。また、preflightリクエストヘッダのOrigin、Access-Control-Request-Method、Access-Control-Request-Headersは、次の本来要求分のリクエスト情報である事が分かります。preflightリクエストに対するレスポンスは、Access-Control-Allow-Origin、Access-Control-Allow-Method、Access-Control-Allow-Headersが設定されています。
(2)本来要求分のリクエスト動作
上図は本来要求分のリクエストヘッダとレスポンスヘッダです。『CORS(そのままリクエストを送る)』方法の場合と同じになり、本来要求分のレスポンスヘッダにAccess-Control-Allow-Originが設定されている事が分かります。