Spring MVC で CORS 設定

# はじめに
Angular と Spring Boot による SPA 開発時、
クライアントサイドとサーバサイドアプリを、
別々のマシンにホスティングする場合の考慮事項をまとめた記事です。

 - Same Origin Policy
 - Cross-Origin Resource Sharing
 - Spring MVC の Cross-Origin Resource Sharing 設定
 - サーバホスト名解決

の４構成でご説明します。

なお、[サンプルのアプリケーション](https://github.com/Masato1986/cross-origin)を公開しています。

# Same Origin Policy
同一オリジンポリシーと訳されます。

下の図は、

 - Angular アプリが https://angular.example.com
 - Spring Boot アプリが https://boot.example.com

でそれぞれ動作している場合を表しています。

![Same Origin Policy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297454/710bc667-1ac9-3ca5-b7f0-e0753b3a436f.png)


Webブラウザからは、https://angular.example.com にアクセスして、
index.html や jsファイルがダウンロードされて、
Webブラウザ上で Angular アプリが動作します。

この時、クライアントサイドはデータの取得を行うためSpring Boot が動作しているサーバ https://boot.example.com に、非同期通信を行いますが、そのリクエストがエラーとなります。

[同一オリジンポリシー - MDN web docs] (https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy) にあるように、
https://angular.example.com から読み込まれたスクリプトファイルから、
https://boot.example.com のリソースへアクセスできないように制限がかけられるためです。

これを Same Origin Policy（同一オリジンポリシー）といいます。

# Cross-Origin Resource Sharing
CORS と略されます。

Same Origin Policy（同一オリジンポリシー）はセキュアなWebアプリケーションを構築する上では重要な概念です。
一方で、クライアントサイドとサーバサイドを別々のマシンで動作させる必要がある場合には、Cross-Origin Resource Sharing（オリジン間リソース共有と訳されます） を利用します。

基本的な仕組みは、HTTPリクエストヘッダとHTTPレスポンスヘッダのやり取りになります。

まず、クライアントサイドからは、Originというリクエストヘッダーを設定してリクエストを送ります。この時の値は、スクリプトファイルの生成元、https://angular.example.com を設定します。

```console
Origin: https://angular.example.com
```

次に、リクエストを受け取ったサーバは、リクエストヘッダーのOriginの値が正当なものか判定します。
もし正当なものであれば、Access-Control-Allow-Originというレスポンスヘッダーを添えて、正常にレスポンスを返却します。

```console
Access-Control-Allow-Origin: https://angular.example.com
```

より詳細な情報は、 [オリジン間リソース共有 (CORS) - MDN web docs](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS) を参照してください。

# Spring MVC の Cross-Origin Resource Sharing 設定
Spring MVC では簡単に Cross-Origin Resource Sharing の設定が行えます。
今回は、

 - コントローラークラス・メソッドに個別にアノテーションで設定する
 - Java Configでアプリケーション全体で設定する

の二つの方法をご紹介します。

なお、環境構築の簡略化のため、Spring Boot を使う事を前提とします。

### コントローラークラス・メソッドに個別にアノテーションで設定する
@CrossOrigin アノテーションを使います。
実際の使い方は下記です。

```java
package com.example.server.web.rest;

import com.example.server.service.TaskService;
import com.example.server.web.response.TaskResponse;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/tasks")
public class TaskRestController {

    private final TaskService todoService;

    public TaskRestController(TaskService todoService) {
        this.todoService = todoService;
    }

    @GetMapping
    @CrossOrigin
    public List<TaskResponse> findAll() {
        return this.todoService.findAll().stream().map(todo -> new TaskResponse(todo)).collect(Collectors.toList());
    }
}
```

異なるオリジンからのアクセスを許容するコントローラーメソッドにアノテーションを付加します。
こうすることによって、Cross-Origin Resource Sharing が有効になります。

ただし、デフォルトの設定では、許容する Origin がワイルドカード、
つまり世界中すべてのWebサイトで生成されたスクリプトからリクエストを許容する設定になっています。

@CrossOrigin アノテーションの origins 属性を使って許容する Origin を設定することができます。

具体的な方法は下記です。

```java
package com.example.server.web.rest;

import com.example.server.service.TaskService;
import com.example.server.web.response.TaskResponse;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/tasks")
public class TaskRestController {

    private final TaskService todoService;

    public TaskRestController(TaskService todoService) {
        this.todoService = todoService;
    }

    @GetMapping
    @CrossOrigin(origins = {"http://localhost:4200"})
    public List<TaskResponse> findAll() {
        return this.todoService.findAll().stream().map(todo -> new TaskResponse(todo)).collect(Collectors.toList());
    }
}
```

文字列型の配列を受け取るようになっていますので、許容する Origin を複数設定することもできます。

他にも、細かな制限をかけることが可能なので、[公式ドキュメント](https://docs.spring.io/spring/docs/5.2.1.RELEASE/spring-framework-reference/web.html#mvc-cors-controller) を参照し、要件に合わせた設定を行ってください。

###  Java Configでアプリケーション全体で設定する
先ほどコントローラーメソッドに設定した @CrossOriginアノテーションは削除しましょう。

下記の Java Config を Spring Boot のコンポーネントスキャンの有効なパッケージに配置してください。

```java
package com.example.server.web.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("http://localhost:4200");
    }
}
```

WebMvcConfigurer は Web関連の Spring の設定をカスタマイズするためのインタフェースです。
@Configuration アノテーションを付加して Java Config として設定して、メソッドをオーバライドして設定をカスタマイズします。

CORS は addCorsMappings というメソッドで引数の CorsRegistry に設定していきます。

上記の例では、 Origin が http://localhost:4200 を許容している設定です。

より細かな設定が可能ですので、[公式ドキュメント](https://docs.spring.io/spring/docs/5.2.1.RELEASE/spring-framework-reference/web.html#mvc-cors-global) を参照し、要件に合わせた設定を行ってください。

# サーバホスト名解決
CORS を扱う場合、開発環境では

|Angular  |Spring Boot  |
|---|---|
|http://localhost:4200  |http://localhost:8080  |

で動作するのに対し、本番環境では例えば

|Angular  |Spring Boot  |
|---|---|
|https://angular.example.com  |https://boot.example.com  |

で動作するといった環境での違いを考慮する必要があります。
つまり、開発環境の Angular アプリは、http://localhost:8080 に、
本番環境のAngular アプリは、https://boot.example.com に非同期通信を行うように切り替えなくてはいけません。

Angularには、環境固有の設定値を持たせることができるので、それを使います。

Angular アプリケーションにはデフォルトで、src/environments 配下に環境ごとの設定ファイルを持ちます。

![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297454/db073fb5-322a-86ec-2d3c-98aaeb98cdc5.png)


 - environment.ts ：開発環境用
 - environment.prod.ts ：本番環境用

の設定ファイルになります。
デフォルトでは、 

```typescript:environment.ts
export const environment = {
  production: false
};
```

```typescript:environment.prod.ts 
export const environment = {
  production: true
};
```

だけが設定されています。
これを下記の様に編集します。

```typescript:environment.ts 
export const environment = {
  production: false,
  apiUrl: 'http://localhost:8080'
};
```

```typescript:environment.prod.ts
export const environment = {
  production: true,
  apiUrl: 'http://boot.example.com'
};
```

apiUrl という設定を追加します。
production はAngularアプリケーションを開発モードで立ち上げるかの判定に利用されるので、残しておきましょう。
次に、非同期通信のリクエストを行うサービスクラスを編集します。

```typescript
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
import {Task} from './task';
import {environment} from "../environments/environment";

@Injectable({
  providedIn: 'root'
})
export class TaskDataService {

  constructor(private http: HttpClient) {
  }

  findAll(): Observable<Task[]> {
    return this.http.get<Task[]>(environment.apiUrl + '/api/tasks');
  }
}
```

非同期通信のリクエストURLを環境設定ファイルから参照するようにします。
Angular CLI の `ng serve` で実行しているときやオプションなしの `ng build` でビルドした場合は、
開発環境用の environment.ts が適用されますが、
`ng build --prod` と本番環境用のオプションを付けてビルドした場合は、
本番環境用の environment.prod.ts が適用されます。

より詳細な情報は、[リファレンス](https://angular.jp/guide/build) を参照してください。

### 補足
- Angular CLI ：Angularアプリの開発をサポートするコマンドライン
- ng serve ：Angular CLIに含まれるAngularアプリの検証サーバ
- ng build ：Angular CLIに含まれるAngularアプリのビルド機能