Spring WebFlux で開発した REST API を Swagger Editor から実行するために CORS を有効にする

概要

Swagger Editor の Try it out から REST API を実行できる様にするために、Spring の設定を変更して CORS を有効にする方法を説明します。但し、Spring MVC ではなく、Spring WebFlux を使う場合の方法です。コードは Kotlin で書かれています。

目次

確認環境

  • Spring Boot 2.0.3
  • Kotlin 1.2.50

参考情報

解説

Swagger Editor で発生するエラー

Swagger Editor の Try it out から REST API を実行すると CORS に関するエラーが発生します。ブラウザの画面の Swagger Editor には、以下のエラーが表示されます。(読みやすくなるよう、途中に改行を入れています。)

Possible cross-origin (CORS) issue?
The URL origin (http://localhost:8080) does not match the page (http://editor.swagger.io).
Check the server returns the correct 'Access-Control-Allow-*' headers.

Spring のデフォルト設定では CORS は無効になっており、swagger.io のドメイン(オリジン)で動作している Web アプリケーションから、REST API を実行するドメイン(オリジン)へのリソース要求は拒否されます。

CORS を有効にする方法

WebFilter を実装したクラスを作成

CORS を有効にするには以下のとおりにします。

import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatus
import org.springframework.web.cors.reactive.CorsUtils
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono

@Component
class CorsFilter : WebFilter {

    private val allowedOrigin = "http://editor.swagger.io"
    private val allowedMethods = "GET, PUT, POST, DELETE, OPTIONS"
    private val allowedHeaders = "Content-Type"
    private val maxAge = "3600"

    override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
        val request = exchange.request
        if (CorsUtils.isCorsRequest(request)) {
            val response = exchange.response
            response.headers.apply {
                add("Access-Control-Allow-Origin", allowedOrigin)
                add("Access-Control-Allow-Methods", allowedMethods)
                add("Access-Control-Allow-Headers", allowedHeaders)
                add("Access-Control-Max-Age", maxAge)
            }
            if (request.method == HttpMethod.OPTIONS) {
                response.statusCode = HttpStatus.OK
                return Mono.empty<Void>()
            }
        }
        return chain.filter(exchange)
    }
}

上記のコードでは WebFilter インターフェースを実装したクラスを作成して、Bean として登録しています。filter メソッドの処理では、request に Origin ヘッダーが設定されているとき (CorsUtils.isCorsRequest(request) が true のとき)、response header に Access-Control-* ヘッダーを追加しています。更に、プリフライト要求の場合には OPTIONS メソッドでの request が送られるため、その場合には response body を空のままで応答します。単純要求の場合には Access-Control-* ヘッダーを追加して、次のフィルタに処理を委譲します。CORS 要求でない場合にも、次のフィルタに処理を委譲します。フィルタが委譲を繰り返し、いずれ応答します。

この WebFilter によって、異なるオリジンから Origin ヘッダーを付加して送信された要求に対して、Access-Control-* ヘッダーを付加した応答が返されます。

Configure で Bean を登録

別の方法として、以下の方法でも設定できます。

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatus
import org.springframework.web.cors.reactive.CorsUtils
import org.springframework.web.server.WebFilter
import reactor.core.publisher.Mono

@Configuration
class CorsConfiguration {

    private final val allowedOrigin = "http://editor.swagger.io"
    private final val allowedMethods = "GET, PUT, POST, DELETE, OPTIONS"
    private final val allowedHeaders = "Content-Type"
    private final val maxAge = "3600"

    @Bean
    fun corsFilter(): WebFilter = WebFilter { exchange, chain ->
        val request = exchange.request
        if (CorsUtils.isCorsRequest(request)) {
            val response = exchange.response
            response.headers.apply {
                add("Access-Control-Allow-Origin", allowedOrigin)
                add("Access-Control-Allow-Methods", allowedMethods)
                add("Access-Control-Allow-Headers", allowedHeaders)
                add("Access-Control-Max-Age", maxAge)
            }
            if (request.method == HttpMethod.OPTIONS) {
                response.statusCode = HttpStatus.OK
                return@WebFilter Mono.empty<Void>()
            }
        }
        return@WebFilter chain.filter(exchange)
    }
}

WebFilter の適用順をカスタマイズしたい場合には、こちらの方法よりも WebFilter を実装したクラスを作成した方が簡単に対応できます。