SpringによるCross Origin Resource Sharing

こんにちは。サイオステクノロジー武井です。

今回は、Spring Bootにて、Cross Origin Resource Sharing(以降、CORS)を行う方法を書いてみます。

まず、その前に、CORSとはどういうものかを説明します。

Cross Origin Resource Sharingとは?

基本的にJava ScriptによるHTTP通信(XMLHttpRequestによるAjax通信)は、Same Origin Policyが適用されます。日本語で言うと同一生成元ポリシーになります。つまり、Ajax通信を行うJava ScriptがあるサイトのURLが、https://www.sios.com/とします。このJava Scriptが、https://api.example.com/messages/というURLのAPIにアクセスしようとすると、Same Origin Policyに違反して、アクセスが拒否されます。

どうして、このような制約を設ける必要があるかというと、それはクロスサイトリクエストフォージェリ(CSRF)対策のためとなります。

CSRFとは?

例えば、Aさんというユーザーがいます。このAさんは、楽しいお買い物.comというショッピングサイトの愛好者です。

Aさんは、いつものように、楽しいお買い物.comで楽しくお買い物をしてました。お買い物サイトは、アクセスしてきたユーザーによって、見せる情報が異なります。例えば、ショッピングカートの中に入ってる商品はユーザーによって異なります。なので、ユーザーを識別するのにセッションという仕組みを用います。通常、セッションIDはCookieの中に保存され、アクセスの都度、HTTPリクエストのCookieヘッダーに乗せて、サイトに渡されます。

そこで、悪意のあるユーザーが、自分のサイトに以下のHTMLを置いたとします。

<script language="javascript">
  var req req = new XMLHttpRequest();
  if (req) {
    req.open('GET', 'https://楽しいお買い物.com/cart'); // (1) 楽しいお買い物サイト.comのショッピングカート表示
    req.onreadystatechange = function() {
      if (req.readyState == 4) {
        sendMail2Attacker(req.responseText); // 悪意ある人へのメール送信
      }
    }
    req.send(null);
  }
</script>

ものすごく適当なJava Scriptですみません。

悪意のあるユーザーは、巧みな手段で、例えばメールなどにこのサイトのリンクを貼り付けるなどして、Aさんにこのサイトにアクセスさせたとします。

すると、ユーザーAさんがお買い物サイトにアクセスした後に、悪意のあるユーザーが用意したページにアクセスすると、ショッピングカートの中身が、悪意のある人にメールで送られてしまいまそうです。

なぜならば、この悪意のあるサイトにアクセスする前に、Aさんは、楽しいお買い物.comにアクセスして、そのサイトのセッションIDがCookieに格納されています。その状態で、悪意のある人が用意したページに行くと、楽しいお買い物サイト.comに、AさんのセッションIDがCookieに乗せられて、送られてしまい、正規なユーザーとして判断されて、Aさんのショッピングカートの中身がレスポンスとして返ってきて、sendMail2Attaker関数(関数の中身はどこかで定義されており、引数で指定した内容を悪意のある人にメールで送信するものです)で悪意のある人にメールで送られてしまいそうです。

先ほどから「そうです」と言っていますが、その通りで、実際には、Aさんのショッピングカートの中身は、悪意のある人にメールで送信されません。先ほど説明したSame Origin Policyが働くからです。

しかしながら、やはり、このSame Origin Policyを超えてアクセスしたいという要望はあると思います。例えば、お天気情報を取得するAPIとかでしょうか。お天気.comというサイトのAPIをXMLHttpRequestで取得して、自社Webサイトに反映させたい場合などです。自社WebサイトのURLと、お天気.comのAPIのURL働くからで異なるので、Same Origin Policyが働き、アクセスが出来ません。

そこで、CORSの登場なわけです。

CORSのしくみ

上記のようなことを実現させるために、CORSという仕様が制定されました。

ざっくり言いますと、以下のような手順になります。

(1) Webサイトに置いてあるJava Scriptは、AjaxなどでAPIに接続する際、そのAPIを提供するサーバー(以降、APIサーバー)に、他のURLからの接続がオッケーかどうかを聞く(preflight request)

(2) もし、APIの提供するサーバーからオッケーという返事が返ってきたら、WebサイトはAPIをコールする。

つまり、実質2回のHTTPリクエストが投げられます。

上記の内容をもっと細かく説明しますと、以下のようになります。

(1) WebサイトはAPIサーバーに以下のリクエストを投げます。これをpreflight requestと言います。

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

(2) APIサーバーは以下のレスポンスを返します。

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

Access-Control-Allow-Originは、このAPIサーバーに接続していい接続元のWebサイトのURLを表します。*の場合は、全てのサイトのからの接続を許可します。接続元を制限したい場合は、以下のように記載します。

Access-Control-Allow-Origin: https://www.sios.com

Access-Control-Allow-Methodsは接続元のWebサイトから送られてくるリクエストのメソッド(POST、GET等)になります。*の場合は全てのメソッドを許可することとなります。

(3) WebサイトのJava Script(正確に言いますとXMLHttpRequest)は、(2)のレスポンスを元に、APIサーバーにリクエストを発行するかを決めます。先ほどのようにAccess-Control-Allow-Originに*があるか、WebサイトのURLが含まれていたら、APIサーバーへのリクエストを発行します。

昔は、JSONPというしくみでクロスサイトなリクエストの発行を実現していました。しかし、JSONPというのは、ある意味、Java Scriptの仕様の裏をかいた、裏ワザ的な手法であります。今後は、上記のような、RFCで制定された手法が主流になってきます。

このような仕組みでクロスサイトなリクエストの発行を制限しています。

CORSを実現するソースコード

CORSを実現するソースコードを実際に書いてみます。例は、JavaのSpringというフレームワークですが、他の言語でも似たような仕組みになると思います。

以下がソースコードです。preflight requestはリクエストごとに毎回処理する必要があるため、Servlet Filterで実現しています。

// pakageやimportは省略しています
@Component
@Order(1) // (1)
public class CORSFilter implements Filter {

	@Autowired
	HttpSession session;

	@Override
	public void destroy() {
		// TODO Auto-generated method stub

	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {

		HttpServletRequest httpsServletRequest = (HttpServletRequest) request;
		HttpServletResponse httpsServletResponse = (HttpServletResponse) response;

		httpsServletResponse.setHeader("Access-Control-Allow-Origin", "*"); // (2)
		httpsServletResponse.setHeader("Access-Control-Allow-Methods", "*"); // (3)

		if (httpsServletRequest.getMethod().equals("OPTIONS")) { // (4)

			httpsServletResponse.setStatus(HttpServletResponse.SC_OK);

			return;

		}

		chain.doFilter(request, response); // (5)

	}

	@Override
	public void init(FilterConfig arg0) throws ServletException {
		// TODO Auto-generated method stub

	}

}

(1)はフィルタの適用順序を表しています。preflight requestは必ず一番最初に処理されないといけないため、他のフィルタよりも一番最初に処理される必要があります。

(2)(3)は、Access-Control-Allow-OriginヘッダとAccess-Control-Allow-Methodsを返しています。

(4)は、メソッドがOPTIONS、つまりこのリクエストがpreflight requestのときだけ、HTTPステータスコード200を返し、returnとしています。こうすると次のフィルタは処理されず、このレスポンスは終了となり、これ以上は処理されます。逆に、リクエストがpreflight request出なかった場合は、(5)に行き、後続のフィルタが処理されます。

以上が、CORSの説明及び実戦投入方法でした。何かのお役に立てたら幸いです。

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

1人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です