Spring Web Reactive | 1. Spring WebFlux | 1.7. CORS
Spring WebFluxを使用すると、CORS(クロスオリジンリソース共有)を処理できる。このセクションではその方法について説明する。
1.7.1. 導入
セキュリティ上の理由から、ブラウザは現在のオリジン外のリソースへのAJAX呼び出しを禁止している。たとえば、あるタブに銀行口座を開き、別のタブにevil.comを開いているとする。evil.comのスクリプトは、認証情報を使用して銀行のAPIへAJAXリクエストを送り、口座からお金を引き出すようなことはできない。
Cross-Origin Resource Sharing(CORS)は、多くのブラウザに実装されているW3C標準であり、IFRAMEやJSONPに基づく安全性が低く強力でない回避策を使うのではなく、許可された種類のクロスドメインリクエストを指定できる。
1.7.2. 処理
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を実装できる。
グローバル設定とローカル設定を結合する規則は、通常は追加的である。たとえば、すべてのグローバルオリジンとすべてのローカルオリジンが使われる。allowCredentialsやmaxAgeのように単一値だけを受け付ける属性の場合、ローカル値がグローバル値を上書きする。詳細はCorsConfiguration#combine(CorsConfiguration)を参照してほしい。
ソースで詳細を確認したり、高度なカスタマイズを行ったりする場合は、次を参照してほしい。
CorsConfigurationCorsProcessor,DefaultCorsProcessorAbstractHandlerMapping
1.7.3. @CrossOrigin
@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. グローバル設定
細かなコントローラメソッドレベル構成に加えて、おそらくグローバルCORS構成も定義する必要がある。URLベースのCorsConfigurationマッピングはHandlerMappingで個別に設定できる。ただし、ほとんどのアプリケーションはWebFlux Java構成を使ってこれを行う。
デフォルトのグローバル構成では、次を使用できる。
- すべてのオリジン。
- すべてのヘッダー。
GET、HEAD、POSTメソッド。
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
組み込みの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)
}