Skip to content

Instantly share code, notes, and snippets.

@samunohito
Created September 14, 2022 11:52
Show Gist options
  • Save samunohito/6000650cffeaf5a317400df1e76a6d62 to your computer and use it in GitHub Desktop.
Save samunohito/6000650cffeaf5a317400df1e76a6d62 to your computer and use it in GitHub Desktop.
HttpServletRequestから得られる情報を使って、CORSの設定を"ある程度"動的に行ってみる
package com.osm.apps.ato_keshi.api.util.request
import com.osm.apps.ato_keshi.api.exception.domain.ForbiddenRuntimeException
import org.springframework.http.HttpMethod
import org.springframework.stereotype.Component
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.servlet.mvc.method.RequestMappingInfo
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@Component
class CorsUtilsComponent(private val requestMappingHandlerMapping: RequestMappingHandlerMapping) {
/**
* [request]から収集した情報をもとに、下記のCORSヘッダを[response]へと付与する。
* - "Access-Control-Allow-Origin"
* - "Access-Control-Allow-Credentials"
* - "Access-Control-Allow-Methods" ※[request]のHTTPメソッドが"OPTIONS"の場合のみ
* - "Access-Control-Allow-Headers" ※[request]のHTTPメソッドが"OPTIONS"の場合のみ
* - "Access-Control-Max-Age" ※[request]のHTTPメソッドが"OPTIONS"の場合のみ
*
* @param request 受信したリクエスト
* @param response ヘッダ設定対象のレスポンス
* @see applyAllowOrigin
* @see applyAllowHeaders
* @see applyAllowMethods
*/
fun applyCorsHeaders(request: HttpServletRequest, response: HttpServletResponse) {
applyAllowOrigin(request, response)
response.setHeader("Access-Control-Allow-Credentials", "true")
if (request.method.lowercase() == HttpMethod.OPTIONS.name.lowercase()) {
applyAllowHeaders(request, response)
applyAllowMethods(request, response)
response.setHeader("Access-Control-Max-Age", "1800")
}
}
/**
* [request]が持つ"Origin"ヘッダの値を取り出し、[response]に"Access-Control-Allow-Origin"ヘッダを設定する。
* [request]に"Origin"ヘッダが含まれていることを前提とするため、値の取得に失敗した場合は例外とする。
*
* @param request 受信したリクエスト
* @param response ヘッダ設定対象のレスポンス
* @throws ForbiddenRuntimeException [request]に"Origin"ヘッダが付与されていない場合
*/
private fun applyAllowOrigin(request: HttpServletRequest, response: HttpServletResponse) {
val origin = request.getHeader("Origin").ifBlank {
throw ForbiddenRuntimeException()
}
response.setHeader("Access-Control-Allow-Origin", origin)
}
/**
* [HttpServletRequest.getRequestURI]と一致するエンドポイントを列挙し、そのエンドポイントが求めているHTTPメソッドの情報を
* "Access-Control-Allow-Methods"ヘッダとして[response]に設定する。
* 同一のパスを持つがHTTPメソッドごとに関数が分かれているようなケースにおいては、それらの関数が要求するHTTPメソッドすべてを収集し、
* "Access-Control-Allow-Methods"の設定値とする。
* また、Preflight requestに対応するため、上記で検出したHTTPメソッド一覧に加えて[RequestMethod.OPTIONS]を無条件で追加する。
*
* @param request 受信したリクエスト
* @param response ヘッダ設定対象のレスポンス
* @see findMappingInfosByPath
*/
private fun applyAllowMethods(request: HttpServletRequest, response: HttpServletResponse) {
val supportRequestMethods = findMappingInfosByPath(request).flatMap { it.methodsCondition.methods }
val allowMethodsText = (supportRequestMethods + listOf(RequestMethod.OPTIONS))
.toSet()
.joinToString(", ") { it.name }
response.setHeader("Access-Control-Allow-Methods", allowMethodsText)
}
/**
* [HttpServletRequest.getRequestURI]と一致するエンドポイントを列挙し、そのエンドポイントが求めているHTTPヘッダを
* "Access-Control-Allow-Headers"ヘッダとして[response]に設定する。
* 同一のパスを持つがHTTPメソッドごとに関数が分かれているようなケースにおいては、それらの関数が要求するHTTPヘッダすべてを収集し、
* "Access-Control-Allow-Headers"の設定値とする。
*
* 求めているHTTPヘッダは、エンドポイントのメソッドが持つパラメータに付与されている[RequestHeader]アノテーションの値から取得される。
* そのため、[RequestHeader]アノテーション経由以外の手段でヘッダの値を引き込んでいる場合は
* この関数以外で"Access-Control-Allow-Headers"の値を設定する必要がある。
*
* @param request 受信したリクエスト
* @param response ヘッダ設定対象のレスポンス
* @see findMappingInfosByPath
*/
private fun applyAllowHeaders(request: HttpServletRequest, response: HttpServletResponse) {
// マッピング情報からはヘッダの情報を取得することが出来ないので、エンドポイントのメソッドに情報からリフレクション経由でヘッダの値を取り出す
val handlerMethods = findMappingInfosByPath(request).mapNotNull { requestMappingHandlerMapping.handlerMethods[it] }
val allowHeadersText = handlerMethods
.flatMap { it.methodParameters.asIterable() }
.mapNotNull { it.getParameterAnnotation(RequestHeader::class.java) }
.map { it.value.ifBlank { it.name }.lowercase() }
.toSet()
.joinToString(", ")
response.setHeader("Access-Control-Allow-Headers", allowHeadersText)
}
/**
* [HttpServletRequest.getRequestURI]と一致([RequestMappingInfo.getDirectPaths]に含まれるかどうかで判断)するパスを持つ
* エンドポイントのマッピング情報を検索するシーケンスを作成する。
*
* @param request 受信したリクエスト
* @return 見つかったマッピング情報を返すシーケンス
*/
private fun findMappingInfosByPath(request: HttpServletRequest): Sequence<RequestMappingInfo> {
return requestMappingHandlerMapping.handlerMethods.keys.asSequence().filter { info ->
info.directPaths.contains(request.requestURI)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment