2018年9月27日木曜日

CORSまとめ

今更ですが、CORS (Cross-Origin Resource Sharing)を色々試していたら、思っていた以上に色々パターンがあることに気づいたので、改めてその扱い方についてまとめてみました。

そもそも

現在のWebブラウザでは、あるWebサイトが持つ情報が別の悪意あるWebサイトに悪用されるのを防ぐために、Same-Origin Policy(日本語では同一生成元ポリシー)が適用されます。

例えば、あるWebサイト https://guiltysite.com をブラウザで表示している時に、このWebページからXMLHttpRequest(以下、XHR)Fetch APIで別のWebサイト https://innocentsite.net からHTTP(S)でデータを読み込もうとすると、エラーになる、というわけです。

しかし、アクセス元が悪意あるWebサイトならともかく、データの連携をする相手として信頼関係ができているWebサイトにまで制限をかけてしまうと不便ですので、データのアクセスを許可できるWebサイトに対してはOriginを越えたアクセスを可能にするための仕組みとして、CORSがあります。

CORSの使い方の例

ここでは、あるWebサイト https://trustedsite.com に対して、別のWebサイト https://usefulapis.net へのHTTP(S)でのアクセスを許可したい場合を例として述べます。

シンプルにデータの読み込みを許可したい場合

単純にXHRFetch APIでのGETPOSTを許可したい場合は、次のようにします。まず、クライアントサイドでは、XHRの場合は特段の工夫は必要なく、Fetch APIの場合はオプションによってCORSを使うことを宣言します。

クライアントのJavaScript(XHR)

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://usefulapis.net/api');
xhr.addEventListener('load', onLoadFunc, false);
xhr.send(null);

クライアントのJavaScript(Fetch)

fetch('https://usefulapis.net/api', {
  mode: 'cors'
}).then(onLoadFunc);

一方、Webサーバ側では、Originを越えたアクセスを許可することをブラウザに明示的に知らせるために、HTTPレスポンスヘッダに適切な情報を付加する必要があります。

まず、ブラウザからサーバに送られるHTTPリクエストヘッダには、Originを越えるアクセスの場合はOriginというフィールドが含まれます。

GET /api HTTP/1.1
Origin: https://trustedsite.com

もし、Originの内容が信頼できるWebサイトのOriginであれば、HTTPレスポンスヘッダに、

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://trustedsite.com

といった内容を追加すれば、ブラウザ側でアクセスが許可されるようになります。なお、このようなシンプルな例に限り、どのWebサイトにもOriginを越えるアクセスを許可することをワイルドカードで指定することが可能です(サブドメイン等の部分指定はできません)

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *

Cookieも許可したい場合

HTTP(S)通信時にCookieの送受信も許可したい場合は、ブラウザとサーバの両方で、もう少々細工が必要となります。まず、ブラウザのJavaScriptでは次のようにします。なお、この例以降では、Access-Control-Allow-Originにおいてワイルドカード指定が許可されなくなりますので、注意が必要です。

クライアントのJavaScript(XHR)

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://usefulapis.net/api');
xhr.withCredentials = true;
xhr.addEventListener('load', onLoadFunc, false);
xhr.send(null);

クライアントのJavaScript(Fetch)

fetch('https://usefulapis.net/api', {
  mode: 'cors',
  credentials: 'include'
}).then(onLoadFunc);

これに対し、サーバ側ではHTTPレスポンスヘッダに次のような内容を追加します。

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://trustedsite.com
Access-Control-Allow-Credentials: true

さらに手の込んだHTTP通信を使いたい場合

CORSの仕様では、次の条件に一つでも当てはまる場合は、実際のHTTPリクエスト(GETPOST)を行う前に、preflight requestとしてOPTIONSリクエストを行うことが定められています。この場合、サーバ側ではGETPOSTに加えてOPTIONSでも同様のCORS対応が必要になりますので、注意が必要です。

·         HTTPリクエストのメソッドがGET, POST, HEAD以外である。

·         HTTPリクエストヘッダにAccept, Accept-Language, Content-Language以外のフィールドが含まれている、あるいは、Content-Typeフィールドにapplication/x-www-form-urlencoded, multipart/form-data, text/plain以外の内容が指定されている。

preflight requestには、次のようなHTTPリクエストヘッダが含まれます。

OPTIONS /api HTTP/1.1
Access-Control-Request-Method: (この後に行うリクエストのHTTPメソッド(GET, POSTなど))

このpreflight requestに対するレスポンスとしては、例えば、少なくとも次のような要領で、Originを越えるアクセスとして許可するHTTPリクエストのメソッドを指定する必要があります。

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://trustedsite.com
Access-Control-Allow-Methods: GET,POST,HEAD,OPTIONS

リクエストに独自のHTTPリクエストヘッダを追加したい場合

例えば、ブラウザ側でX-MyRequestX-MyOptionというヘッダを追加したとします。

クライアントのJavaScript(XHR)

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://usefulapis.net/api');
xhr.withCredentials = true;
xhr.setRequestHeader('X-MyRequest', 'this-is-cors-test');
xhr.setRequestHeader('X-MyOption', 'my-option');
xhr.addEventListener('load', onLoadFunc, false);
xhr.send(null);

クライアントのJavaScript(Fetch)

fetch('https://usefulapis.net/api', {
  method: 'GET',
  mode: 'cors',
  credentials: 'include',
  headers: {
    'X-MyRequest': 'this-is-cors-test',
    'X-MyOption': 'my-option'
  }
}).then(onLoadFunc);

この場合、まず次のようなHTTPリクエストヘッダを含むpreflight requestがブラウザからサーバに送られます。

OPTIONS /api HTTP/1.1
Origin: https://trustedsite.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: X-MyRequest,X-MyOption

サーバ側では、これらのリクエストヘッダに示されているメソッドとヘッダを許可するかどうかを判断して、レスポンスヘッダを返します。Access-Control-Allow-Methodsで指定されたメソッドと、Access-Control-Allow-Headersで指定されたヘッダが、この後ブラウザが実際に送るHTTPリクエストに許可されます。(該当するヘッダはpreflightと実際のリクエストの両方で必要になります。)

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://trustedsite.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,POST,HEAD,OPTIONS
Access-Control-Allow-Headers: X-MyRequest,X-MyOption

レスポンスに独自のHTTPレスポンスヘッダを追加してブラウザから読み出したい場合

Originを越えるアクセスの場合、例えば、ブラウザ側のコードが、

クライアントのJavaScript(XHR)

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://usefulapis.net/api');
xhr.withCredentials = true;
xhr.setRequestHeader('X-MyRequest', 'this-is-cors-test');
xhr.setRequestHeader('X-MyOption', 'my-option');
xhr.addEventListener('load', onLoadFunc, false);
xhr.send(null);
 
function onLoadFunc() {
  var myResponse = xhr.getResponseHeader('X-MyResponse');
  var myOption = xhr.getResponseHeader('X-MyOption');
}

クライアントのJavaScript(Fetch)

fetch('https://usefulapis.net/api', {
  method: 'GET',
  mode: 'cors',
  credentials: 'include',
  headers: {
    'X-MyRequest': 'this-is-cors-test',
    'X-MyOption': 'my-option'
  }
}).then(onLoadFunc);
 
function onLoadFunc(response) {
  var myResponse = response.headers.get('X-MyResponse');
  var myOption = response.headers.get('X-MyOption');
}

となっていて、これに対してサーバ側からは、

HTTP/1.1 200 OK
X-MyResponse: this-is-successful-response
X-MyOptions: good-result

のような独自レスポンスヘッダをブラウザに返そうとしている場合、ブラウザがこれらのレスポンスヘッダの内容を取得しようとすると、セキュアではないヘッダにアクセスしようとしたものとみなされて、アクセスが許可されないようになっています。(アクセスが許可されるレスポンスヘッダは、Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragmaとなっているようです。)

このような独自レスポンスヘッダへのアクセスをブラウザに許可するには、許可したいレスポンスヘッダをAccess-Control-Expose-Headersで指定します。

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://trustedsite.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,POST,HEAD,OPTIONS
Access-Control-Allow-Headers: X-MyRequest,X-MyOption
Access-Control-Expose-Headers: X-MyResponse,X-MyOption

なお、当然ながら、Set-CookieSet-Cookie2Access-Control-Expose-Headersで指定してもXHRFetch APIで読むことができません。

ところで、preflight requestは毎回行われるのか?

preflight requestには、サーバ側からブラウザにキャッシュさせる有効期限を指定することが出来ます。この期限内であれば、最初のpreflight requestがこの後の同じURLに対するHTTPリクエストにも適用されるようになります。

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://trustedsite.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,POST,HEAD,OPTIONS
Access-Control-Allow-Headers: X-MyRequest,X-MyOption
Access-Control-Expose-Headers: X-MyResponse,X-MyOption
Access-Control-Max-Age: 864000

Access-Control-Max-Ageには有効期限を秒単位で指定します。上記の例では、10日間(10()×24(時間)×60()×60() = 864,000())となっています。

 

https://qiita.com/tomoyukilabs/items/81698edd5812ff6acb34

 

0 件のコメント:

コメントを投稿