PlayFrameworkのCORS Filterでオリジンを正規表現でマッチングする

PlayFrameworkのCORS Filterでオリジンのマッチングに正規表現を使います。
「自社のサブドメインは全て許可」のような設定ができるようになります。

前提

基本的な Scala + Sbt + PlayFramework のプロジェクトで、コントローラーとconfは以下の通り。

app/controllers/HomeController.scala
package controllers import javax.inject._ import play.api._ import play.api.mvc._ class HomeController @Inject() (val controllerComponents: ControllerComponents) extends BaseController { def index(): Action[AnyContent] = Action { implicit request: Request[AnyContent] => Ok("OK") } }
conf/application.conf
play.filters.enabled += "play.filters.cors.CORSFilter" play.filters.cors { allowedOrigins = ["https://blog.murosan.dev"] allowedHttpMethods = ["GET", "POST"] allowedHttpHeaders = [ "Accept", "Accept-Language", "Content-Type", "Keep-Alive", "Origin", "User-Agent" ] }
conf/routs
GET / controllers.HomeController.index()

この状態で起動してCORSが効いているか確認すると、

bash
$ curl localhost:9000 -H 'Origin: https://blog.murosan.dev' -I HTTP/1.1 200 OK Access-Control-Allow-Origin: https://blog.murosan.dev Access-Control-Allow-Credentials: true ... $ curl localhost:9000 -H 'Origin: https://blog2.murosan.dev' -I HTTP/1.1 403 Forbidden ...

しっかり動作している。

Originを正規表現でマッチングさせる

先程ブロックされたhttps://blog2.murosan.devを許可するには、play.filters.cors.allowedOriginsに追加すれば良いですが、サブドメインの数が多くなると大変です。

conf/application.conf
play.filters.cors { // 数が多いと大変 - allowedOrigins = ["https://blog.murosan.dev"] + allowedOrigins = ["https://blog.murosan.dev", "https://blog2.murosan.dev"] allowedHttpMethods = ["GET", "POST"] ... }

そこで、正規表現を用いて全てのサブドメインを許可するように設定してみます。

PlayFrameworkのビルトインCORSFilterを拡張して、独自のCORSFilterを作ります。

app/filters/CORSFilter.scala
package filters import play.api.Configuration import play.api.mvc.EssentialAction import play.filters.cors.CORSConfig import play.filters.cors.CORSConfig.Origins import play.http.HttpErrorHandler import javax.inject.{Inject, Singleton} import scala.util.matching.Regex @Singleton class CORSFilter @Inject() (configuration: Configuration) extends play.filters.cors.CORSFilter(corsConfig = CORSFilter.conf(configuration)) object CORSFilter { def conf(configuration: Configuration): CORSConfig = { // allowedOriginsをapplication.confから取得して正規表現に変える val origins = configuration.get[Seq[String]]("play.filters.cors.allowedOrigins").map(_.r) // configurationからCORSConfigを作成 val default = CORSConfig.fromConfiguration(configuration) // Originのマッチングに正規表現を使うようにする val matching = Origins.Matching { (origin: String) => origins.exists(_.matches(origin)) } default.copy(allowedOrigins = matching) } }
conf/application.conf
- play.filters.enabled += "play.filters.cors.CORSFilter" + play.filters.enabled += "filters.CORSFilter" play.filters.cors { - allowedOrigins = ["https://blog.murosan.dev"] + allowedOrigins = ["""(\Ahttps://[\w.-]+\.murosan\.dev\z)"""] allowedHttpMethods = ["GET", "POST"] ... }

正規表現の書き方や、allowedOriginsの数によっては処理時間が増えてしまうので注意が必要ですが、 このようにCORSConfigを書き換えると正規表現でマッチングできます。

ちなみにPlayFrameworkのデフォルトのCORSFilterはシンプルなSet[String]でマッチングしていました。

確認してみます。

bash
$ curl localhost:9000 -H 'Origin: https://blog.murosan.dev' -I HTTP/1.1 200 OK Access-Control-Allow-Origin: https://blog.murosan.dev Access-Control-Allow-Credentials: true ... $ curl localhost:9000 -H 'Origin: https://blog2.murosan.dev' -I HTTP/1.1 200 OK Access-Control-Allow-Origin: https://blog2.murosan.dev Access-Control-Allow-Credentials: true ... $ curl localhost:9000 -H 'Origin: https://abc.murosan.dev' -I HTTP/1.1 200 OK Access-Control-Allow-Origin: https://abc.murosan.dev Access-Control-Allow-Credentials: true ... # 同一オリジンはもちろんOK $ curl localhost:9000 -H 'Origin: http://localhost:9000' -I HTTP/1.1 200 OK # Preflight Request $ curl localhost:9000 -X OPTIONS \ -H 'Origin: https://blog.murosan.dev' \ -H 'Access-Control-Request-Method: GET' \ -H 'Access-Control-Request-Headers: Accept,Content-Type,User-Agent' -v ... < HTTP/1.1 200 OK < Access-Control-Max-Age: 3600 < Access-Control-Allow-Origin: https://blog.murosan.dev < Access-Control-Allow-Headers: accept,content-type,user-agent < Access-Control-Allow-Methods: GET < Access-Control-Allow-Credentials: true ... # ポート付きは許可していない $ curl localhost:9000 -H 'Origin: https://abc.murosan.dev:3000' -I HTTP/1.1 403 Forbidden # 不正なサブドメイン $ curl localhost:9000 -H 'Origin: https://.murosan.dev' -I HTTP/1.1 403 Forbidden # 末尾に.comがついている $ curl localhost:9000 -H 'Origin: https://abc.murosan.dev.com' -I HTTP/1.1 403 Forbidden # http (not https) $ curl localhost:9000 -H 'Origin: http://abc.murosan.dev' -I HTTP/1.1 403 Forbidden

ローカルはポートも許可する

conf/application.local.conf
play.filters.cors.allowedOrigins = ["""(\Ahttps?://localhost:\d+\z)"""]
conf/application.conf
play.filters.enabled += "filters.CORSFilter" play.filters.cors { ... } +include "application.local.conf"

Filterというか、confで上書きすればOKです。