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です。