Spring Web Reactive | 1. Spring WebFlux | 1.7. CORS

Web MVC

Spring WebFluxを使用すると、CORS(クロスオリジンリソース共有)を処理できる。このセクションではその方法について説明する。

1.7.1. 導入

Web MVC

セキュリティ上の理由から、ブラウザは現在のオリジン外のリソースへのAJAX呼び出しを禁止している。たとえば、あるタブに銀行口座を開き、別のタブにevil.comを開いているとする。evil.comのスクリプトは、認証情報を使用して銀行のAPIへAJAXリクエストを送り、口座からお金を引き出すようなことはできない。

Cross-Origin Resource Sharing(CORS)は、多くのブラウザに実装されているW3C標準であり、IFRAMEやJSONPに基づく安全性が低く強力でない回避策を使うのではなく、許可された種類のクロスドメインリクエストを指定できる。

1.7.2. 処理

Web MVC

CORS仕様は、プリフライト(preflight)、シンプル(simple)、実際のリクエストを区別する。CORSがどのように動作するかを学ぶには、他の多くの資料と同様にこの記事を参照するか、詳細について仕様を確認するとよい。

Spring WebFluxのHandlerMapping実装は、CORSの組み込みサポートを提供する。リクエストを通常どおりハンドラへマッピングした後、HandlerMappingは指定されたリクエストとハンドラのCORS設定を確認し、処理を実行する。プリフライトリクエストは直接処理されるが、シンプルおよび実際のCORSリクエストはインターセプトされ、検証され、必要なCORSレスポンスヘッダーが設定される。

クロスオリジンリクエストを有効化するには、つまりOriginヘッダーが存在し、リクエストのホストと異なる場合は、明示的に宣言されたCORS設定が必要である。一致するCORS設定が見つからない場合、プリフライトリクエストは拒否される。CORSヘッダーはシンプルおよび実際のCORSリクエストのレスポンスには追加されないため、ブラウザはそれらを拒否する。

HandlerMappingは、URLパターンベースのCorsConfigurationマッピングで個別に構成できる。ほとんどの場合、アプリケーションはWebFlux Java構成を使ってこれらのマッピングを宣言する。これにより、1つのグローバルマップがすべてのHandlerMapping実装に渡される。

HandlerMappingレベルのグローバルCORS構成と、より細かなハンドラレベルのCORS構成を組み合わせることができる。たとえば、アノテーション付きコントローラクラスやメソッドレベルの@CrossOriginアノテーションを使用でき、他のハンドラはCorsConfigurationSourceを実装できる。

グローバル設定とローカル設定を結合する規則は、通常は追加的である。たとえば、すべてのグローバルオリジンとすべてのローカルオリジンが使われる。allowCredentialsmaxAgeのように単一値だけを受け付ける属性の場合、ローカル値がグローバル値を上書きする。詳細はCorsConfiguration#combine(CorsConfiguration)を参照してほしい。

ソースで詳細を確認したり、高度なカスタマイズを行ったりする場合は、次を参照してほしい。

  • CorsConfiguration
  • CorsProcessor, DefaultCorsProcessor
  • AbstractHandlerMapping

1.7.3. @CrossOrigin

Web MVC

@CrossOriginアノテーションは、次の例のようなアノテーション付きコントローラメソッドでクロスオリジンリクエストを有効化する。

Java

@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin
    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        // ...
    }
}

Kotlin

@RestController
@RequestMapping("/account")
class AccountController {

    @CrossOrigin
    @GetMapping("/{id}")
    suspend fun retrieve(@PathVariable id: Long): Account {
        // ...
    }

    @DeleteMapping("/{id}")
    suspend fun remove(@PathVariable id: Long) {
        // ...
    }
}

デフォルトでは、@CrossOriginは次を許可する。

  • すべてのオリジン。
  • すべてのヘッダー。
  • コントローラメソッドがマッピングされるすべてのHTTPメソッド。

allowCredentialsはデフォルトでは有効化されていない。これは、CookieやCSRFトークンなどユーザー固有の機密情報を公開する信頼レベルを設定するものであり、適切な場合にのみ使用すべきだからである。有効にする場合、allowOriginsは1つ以上の特定ドメインに設定する必要がある。ただし、特別な値"*"は設定してはならない。または、allowOriginPatterns属性を使用して動的なオリジン集合に一致させることができる。

maxAgeは30分に設定されている。

@CrossOriginはクラスレベルでサポートされるすべてのメソッドに継承される。次の例では特定のドメインを指定し、maxAgeを1時間に設定している。

Java

@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        // ...
    }
}

Kotlin

@CrossOrigin("https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {

    @GetMapping("/{id}")
    suspend fun retrieve(@PathVariable id: Long): Account {
        // ...
    }

    @DeleteMapping("/{id}")
    suspend fun remove(@PathVariable id: Long) {
        // ...
    }
}

次の例のように、@CrossOriginはクラスレベルとメソッドレベルの両方で使用できる。

Java

@CrossOrigin(maxAge = 3600)   // (1)
@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin("https://domain2.com")   // (2)
    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        // ...
    }
}

Kotlin

@CrossOrigin(maxAge = 3600)   // (1)
@RestController
@RequestMapping("/account")
class AccountController {

    @CrossOrigin("https://domain2.com")   // (2)
    @GetMapping("/{id}")
    suspend fun retrieve(@PathVariable id: Long): Account {
        // ...
    }

    @DeleteMapping("/{id}")
    suspend fun remove(@PathVariable id: Long) {
        // ...
    }
}
  • (1) クラスレベルで@CrossOriginを使用する。
  • (2) メソッドレベルで@CrossOriginを使用する。

1.7.4. グローバル設定

Web MVC

細かなコントローラメソッドレベル構成に加えて、おそらくグローバルCORS構成も定義する必要がある。URLベースのCorsConfigurationマッピングはHandlerMappingで個別に設定できる。ただし、ほとんどのアプリケーションはWebFlux Java構成を使ってこれを行う。

デフォルトのグローバル構成では、次を使用できる。

  • すべてのオリジン。
  • すべてのヘッダー。
  • GETHEADPOSTメソッド。

allowedCredentialsはデフォルトでは有効化されていない。これは、CookieやCSRFトークンなどユーザー固有の機密情報を公開する信頼レベルを設定するものであり、適切な場合にのみ使用すべきだからである。有効化する場合は、allowOriginsを1つ以上の特定ドメインに設定する必要がある。ただし、特別な値"*"は設定してはならない。または、allowOriginPatterns属性を使用して動的なオリジン集合に一致させることができる。

maxAgeは30分に設定されている。

WebFlux Java構成でCORSを有効化するには、次の例のようにCorsRegistryコールバックを使用できる。

Java

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {

        registry.addMapping("/api/**")
            .allowedOrigins("https://domain2.com")
            .allowedMethods("PUT", "DELETE")
            .allowedHeaders("header1", "header2", "header3")
            .exposedHeaders("header1", "header2")
            .allowCredentials(true).maxAge(3600);

        // Add more mappings...
    }
}

Kotlin

@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addCorsMappings(registry: CorsRegistry) {

        registry.addMapping("/api/**")
                .allowedOrigins("https://domain2.com")
                .allowedMethods("PUT", "DELETE")
                .allowedHeaders("header1", "header2", "header3")
                .exposedHeaders("header1", "header2")
                .allowCredentials(true).maxAge(3600)

        // Add more mappings...
    }
}

1.7.5. CORS WebFilter

Web MVC

組み込みのCorsWebFilterを使用してCORSサポートを適用できる。これは関数エンドポイントに適している。

CorsFilterをSpring Securityで使用しようとしている場合、Spring SecurityにはCORSのサポートが含まれている点に注意してほしい。

フィルタを構成するには、次の例のようにCorsWebFilter Beanを宣言し、CorsConfigurationSourceをコンストラクタへ渡すことができる。

Java

@Bean
CorsWebFilter corsFilter() {

    CorsConfiguration config = new CorsConfiguration();

    // Possibly...
    // config.applyPermitDefaultValues()

    config.setAllowCredentials(true);
    config.addAllowedOrigin("https://domain1.com");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);

    return new CorsWebFilter(source);
}

Kotlin

@Bean
fun corsFilter(): CorsWebFilter {

    val config = CorsConfiguration()

    // Possibly...
    // config.applyPermitDefaultValues()

    config.allowCredentials = true
    config.addAllowedOrigin("https://domain1.com")
    config.addAllowedHeader("*")
    config.addAllowedMethod("*")

    val source = UrlBasedCorsConfigurationSource().apply {
        registerCorsConfiguration("/**", config)
    }
    return CorsWebFilter(source)
}