Введение
Очень редко, но иногда бывает необходимо использовать дополнительный сервлет рядом со стандартным Dispatcher servlet-ом Spring Web MVC. В данной статье будет рассмотрено подключение сервлета рядом с dispatcher servlet, а также настройка Spring Security.
Немного теории
Несмотря на то, что казалось бы Spring Framework - отдельная от Jakarta EE экосистема, в части инфраструктуры Spring опирается на решения Jakarta EE. Spring Web MVC в стандартном, нереактивном варианте предполагает реализацию через джакартовские сервлеты. В составе Spring Web MVC включен Dispatcher servlet, ответственный за выполнение роли контроллера модели MVC.
Маршрутизация входящих запросов до конкретных сервлетов выполняется в Jakarta EE на основе конфигурации в web.xml. Само собой, есть способ и динамической программной конфигурации, что и используется в Spring.
Фильтрация запросов Spring Security осуществляется на основе цепочки виртуальных фильтров, через которую пропускаются любые запросы, входящие в приложение.
Реализация
В качестве практической иллюстрации приведем пример реализации прокси-сервлета, перенаправляющего запросы согласно конфигурации на другие серверы.
class ProxyServlet(
private val objectMapper: ObjectMapper,
private val proxyProperties: ProxyProperties,
private val proxyManager: ProxyManager,
private val corsSupport: CorsSupport,
private val securitySupport: PassUserInfoAndTrackingHeadersSupport,
): HttpServlet() {
// not override service(), because service() has additional logic for 304 status code for doGet()
override fun doGet(req: HttpServletRequest?, resp: HttpServletResponse?) = proxyRequest(req!!, resp!!)
override fun doHead(req: HttpServletRequest?, resp: HttpServletResponse?) = proxyRequest(req!!, resp!!)
override fun doPost(req: HttpServletRequest?, resp: HttpServletResponse?) = proxyRequest(req!!, resp!!)
override fun doPut(req: HttpServletRequest?, resp: HttpServletResponse?) = proxyRequest(req!!, resp!!)
override fun doDelete(req: HttpServletRequest?, resp: HttpServletResponse?) = proxyRequest(req!!, resp!!)
override fun doOptions(req: HttpServletRequest?, resp: HttpServletResponse?) = proxyRequest(req!!, resp!!)
override fun doTrace(req: HttpServletRequest?, resp: HttpServletResponse?) = proxyRequest(req!!, resp!!)
private fun doPatch(req: HttpServletRequest?, resp: HttpServletResponse?) = proxyRequest(req!!, resp!!)
// HttpServlet doesn't support PATCH method -- implement it by overriding service()
override fun service(req: HttpServletRequest?, resp: HttpServletResponse?) {
val method = req!!.method
if (method == "PATCH") {
doPatch(req, resp)
} else super.service(req, resp)
}
private val exceptionHandler = ProxyExceptionHandler()
private val defaultExceptionHandler = DefaultExceptionHandler()
private fun proxyRequest(request: HttpServletRequest, response: HttpServletResponse) {
if (request.method == HttpMethod.OPTIONS.name()) {
if (proxyProperties.cors.enabled) {
corsSupport.serveCorsPreflightRequest(request, response)
}
return
}
var microserviceResponse: ByteResponse
try {
var tryCounter = 1
val chosenProxyDefinition = proxyManager.chooseProxyDefinition(request)
val downstreamService = chosenProxyDefinition.service
val baseUrl = chosenProxyDefinition.baseUrl
val path = request.pathInfo
val method = request.method
while (true) {
try {
microserviceResponse = executeRequest(
baseUrl = baseUrl,
path = path,
method = method,
httpServletRequest = request,
)
log.trace(
"Successfully executed request: ({}) {} {}{} - {}",
downstreamService,
method,
baseUrl,
path,
microserviceResponse.statusCode.toString()
)
break
} catch (e: RestClientException) {
if (proxyProperties.retries > 0 && tryCounter - 1 < proxyProperties.retries) {
log.error("RestClientException occurred while executing request ($downstreamService) $method $baseUrl$path. " +
"Maybe downstream service unavailable? Trying to retry the request " +
"(tryNumber: ${tryCounter}, retriesConfigured: ${proxyProperties.retries})")
tryCounter++
continue
}
log.error("RestClientException occurred while executing request ($downstreamService) $method $baseUrl$path. " +
"Maybe downstream service unavailable? Retries are not configured or have been executed " +
"(tryNumber: ${tryCounter}, retriesConfigured: ${proxyProperties.retries})")
val responseEntity = defaultExceptionHandler.handleOthers(e)
val extendedErrorResponse = ExtendedErrorResponse(
statusCode = responseEntity.statusCode,
errorResponse = responseEntity.body as ErrorResponse,
)
writeExtendedErrorResponse(
extendedErrorResponse = extendedErrorResponse,
request = request,
response = response
)
return
}
}
} catch (e: ProxyException) {
val extendedErrorResponse = exceptionHandler.handle(e)
writeExtendedErrorResponse(
extendedErrorResponse = extendedErrorResponse,
request = request,
response = response
)
return
}
writeResponse(
microserviceResponse = microserviceResponse,
request = request,
response = response,
)
}
private fun executeRequest(
baseUrl: String,
path: String,
method: String,
httpServletRequest: HttpServletRequest,
): ByteResponse {
val params = if (httpServletRequest.parameterMap.isEmpty()) "" else {
"?${
httpServletRequest.parameterMap
.entries
.flatMap { param -> param.value.toList().map { param.key to it } }
.joinToString(separator = "&") { entry -> "${entry.first}=${entry.second}" }
}"
}
val uri = URI.create("$baseUrl$path$params")
val httpMethod = HttpMethod.valueOf(method)
val restTemplate = configureRestTemplate()
if (method == HttpMethod.GET.name() || method == HttpMethod.DELETE.name()) {
return restTemplate.execute(
uri,
httpMethod,
{ clientHttpRequest -> securitySupport.provideUserInfoHeaders(clientHttpRequest)},
) {
ByteResponse(
statusCode = it.statusCode,
contentType = it.headers.contentType,
content = it.body.readAllBytes()
)
}!!
}
if (method == HttpMethod.POST.name() || method == HttpMethod.PUT.name() || method == HttpMethod.PATCH.name()) {
val bodyRaw = httpServletRequest.inputStream.readAllBytes()
val contentType = httpServletRequest.contentType
return restTemplate.execute(
uri,
httpMethod,
{ clientHttpRequest ->
run {
clientHttpRequest.body.write(bodyRaw)
clientHttpRequest.headers.contentType = contentType?.let { MediaType.parseMediaType(contentType) }
securitySupport.provideUserInfoHeaders(clientHttpRequest)
}
}
) {
ByteResponse(
statusCode = it.statusCode,
contentType = it.headers.contentType,
content = it.body.readAllBytes()
)
}!!
}
throw MethodNotSupportedToBeProxiedException(method)
}
private fun configureRestTemplate(): RestTemplate =
RestTemplate().apply {
errorHandler = MutingResponseErrorHandler()
requestFactory = HttpComponentsClientHttpRequestFactory().apply {
setConnectTimeout(300) // TODO: maybe make it configurable?
}
}
private fun writeResponse(
microserviceResponse: ByteResponse,
request: HttpServletRequest,
response: HttpServletResponse
) {
if (proxyProperties.cors.enabled) {
corsSupport.addCorsHeadersToResponse(request, response)
}
response.status = microserviceResponse.statusCode.value()
response.contentType = microserviceResponse.contentType?.toString()
response.outputStream.write(microserviceResponse.content)
}
private fun writeExtendedErrorResponse(
extendedErrorResponse: ExtendedErrorResponse,
request: HttpServletRequest,
response: HttpServletResponse,
) {
if (proxyProperties.cors.enabled) {
corsSupport.addCorsHeadersToResponse(request, response)
}
response.status = extendedErrorResponse.statusCode.value()
response.contentType = MediaType.APPLICATION_JSON_VALUE
response.outputStream.write(objectMapper.writeValueAsBytes(extendedErrorResponse.errorResponse))
}
}
Пример с одной стороны получился достаточно большой, с другой стороны он обрезан для краткости :) В примере не приведен исходный код классов ProxyManager, CorsSupport, PassUserInfoAndTrackingHeadersSupport, содержащих узконаправленную логику. По сути для цели иллюстрации именно способа настройки своего сервлета рядом с dispatcher servlet-ом данный пример можно детально не изучать, а воспринимать чисто как сервлет, содержащий определенную прикладную логику.
Для того чтобы объяснить Spring-у, что имеется дополнительный сервлет, нам потребуется также конфигурация.
@Configuration
class ProxyConfig {
@Bean
@ConditionalOnProperty("proxy.enabled")
fun proxyServlet(
objectMapper: ObjectMapper,
proxyProperties: ProxyProperties,
proxyManager: ProxyManager,
corsSupport: CorsSupport,
securitySupport: PassUserInfoAndTrackingHeadersSupport,
): ServletRegistrationBean<ProxyServlet>? {
return ServletRegistrationBean(
ProxyServlet(objectMapper, proxyProperties, proxyManager, corsSupport, securitySupport),
"/proxy/*"
)
}
}
С точки зрения настройки Spring Security придется повозиться побольше, так как, как обычно, со спринговским секурити все нетривиально и неочевидно… Нам потребуется сконфигурировать два SecurityFilterChain-а по одному для Dispatcher сервлета и своего собственного.
private fun configureHttpSecurity(
http: HttpSecurity,
securityRealms: SecurityRealms,
interceptor: HandlerMappingIntrospector? = null,
keycloakProperties: KeycloakSecurityProperties,
devSecurityProperties: DevSecurityProperties? = null,
authorizeCustomizer: AuthorizeHttpRequestsDsl.() -> Unit = {}
): SecurityFilterChain {
if (securityRealms == SecurityRealms.API && interceptor == null) {
throw RuntimeException("When configuring security for API realm you have to provide HandlerMappingIntrospector")
}
http {
if (securityRealms == SecurityRealms.API) {
securityMatcher(mvc(interceptor!!, securityRealms.pattern))
cors { } // CORS rules are configured using com.newdex.services.common.cors.AllowedCorsConfig.kt
}
if (securityRealms == SecurityRealms.PROXY) {
securityMatcher(ant(securityRealms.pattern))
cors { disable() } // CORS rules for /proxy/** configured manually in ProxyServlet
}
csrf { disable() } // we don't need CSRF protection, because we don't use sessions
sessionManagement {
sessionCreationPolicy = SessionCreationPolicy.STATELESS
}
authorizeHttpRequests {
if (securityRealms == SecurityRealms.API) {
interceptor!!
authorize(mvc(interceptor, "/v1/sso/**"), permitAll)
authorize(mvc(interceptor, HttpMethod.GET, "/v1/courses/**"), hasAuthority(NewdexPermissions.COURSE_VIEWER.name))
authorize(mvc(interceptor, HttpMethod.GET, "/v1/lessons/*"), hasAuthority(NewdexPermissions.COURSE_VIEWER.name))
authorize(mvc(interceptor, HttpMethod.POST, "/v1/lessons/*/blocks/*/move-to-position"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(mvc(interceptor, HttpMethod.POST, "/v1/lessons/*/blocks"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(mvc(interceptor, HttpMethod.PUT, "/v1/lessons/*/blocks/*"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(mvc(interceptor, HttpMethod.GET, "/v1/course-grades/by-course/*"), hasAuthority(NewdexPermissions.COURSE_VIEWER.name))
authorize(mvc(interceptor, HttpMethod.GET, "/v1/course-grades/by-course/*/graded-blocks"), hasAuthority(NewdexPermissions.COURSE_VIEWER.name))
authorize(mvc(interceptor, HttpMethod.GET, "/v1/course-grades/by-group/*/by-course/*"), hasAuthority(NewdexPermissions.INSTRUCTOR.name))
authorize(mvc(interceptor, HttpMethod.GET, "/v1/courses/*/chapters"), hasAuthority(NewdexPermissions.COURSE_VIEWER.name))
authorize(mvc(interceptor, HttpMethod.GET, "/v1/courses/*/chapters/*/modules"), hasAuthority(NewdexPermissions.COURSE_VIEWER.name))
authorize(mvc(interceptor, HttpMethod.GET, "/v1/courses/*/chapters/*/modules/*/lessons"), hasAuthority(NewdexPermissions.COURSE_VIEWER.name))
authorize(mvc(interceptor, HttpMethod.POST, "/v1/lessons/*/render"), hasAuthority(NewdexPermissions.COURSE_VIEWER.name))
authorize(mvc(interceptor, HttpMethod.GET, "/v1/grading-policies/*"), hasAuthority(NewdexPermissions.COURSE_LISTENER.name))
authorize(mvc(interceptor, HttpMethod.POST, "/v1/grading-policies/*"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(mvc(interceptor, HttpMethod.PATCH, "/v1/grading-policies/*/module-policies"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(mvc(interceptor, HttpMethod.PATCH, "/v1/grading-policies/*/task-policies"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(mvc(interceptor, HttpMethod.POST, "/proxy/api/v1/programs/filtered"), hasAnyAuthority(NewdexPermissions.PROGRAM_EDITOR.name, NewdexPermissions.INSTRUCTOR.name))
authorize(mvc(interceptor, HttpMethod.GET, "/v1/programs/*"), hasAuthority(NewdexPermissions.COURSE_VIEWER.name))
authorize(mvc(interceptor, HttpMethod.PUT, "/v1/programs/courses"), hasAuthority(NewdexPermissions.PROGRAM_EDITOR.name))
authorize(mvc(interceptor, HttpMethod.PATCH, "/v1/programs/teachers"), hasAuthority(NewdexPermissions.PROGRAM_EDITOR.name))
authorize(mvc(interceptor, HttpMethod.GET, "/v1/users/students"), hasAnyAuthority(NewdexPermissions.PROGRAM_EDITOR.name, NewdexPermissions.INSTRUCTOR.name))
authorize(mvc(interceptor, HttpMethod.GET, "/v1/users/teachers"), hasAnyAuthority(NewdexPermissions.PROGRAM_EDITOR.name, NewdexPermissions.INSTRUCTOR.name))
authorize(mvc(interceptor, HttpMethod.GET, "/v1/learning/*"), hasAuthority(NewdexPermissions.COURSE_LISTENER.name))
authorize(mvc(interceptor, HttpMethod.GET, "/v1/groups/*/grading-policies"), hasAuthority(NewdexPermissions.INSTRUCTOR.name))
authorize(mvc(interceptor,HttpMethod.GET, "/v1/groups/*"),hasAnyAuthority(NewdexPermissions.INSTRUCTOR.name))
authorize(mvc(interceptor,HttpMethod.PATCH, "/v1/groups/*/students"),hasAnyAuthority(NewdexPermissions.INSTRUCTOR.name))
authorize(mvc(interceptor,HttpMethod.PATCH, "/v1/groups/*/students/force"),hasAnyAuthority(NewdexPermissions.INSTRUCTOR.name))
authorize(mvc(interceptor,HttpMethod.POST, "/v1/groups/*/students/filtered"),hasAnyAuthority(NewdexPermissions.INSTRUCTOR.name))
authorize(mvc(interceptor,HttpMethod.GET, "/v1/groups/*/students"),hasAnyAuthority(NewdexPermissions.INSTRUCTOR.name))
authorize(mvc(interceptor,HttpMethod.GET, "/v1/groups/*/schedule/by-course/{courseId}"),hasAnyAuthority(NewdexPermissions.INSTRUCTOR.name))
authorize(mvc(interceptor,HttpMethod.GET,"/v1/groups/*/compare/*"), hasAuthority(NewdexPermissions.INSTRUCTOR.name))
// Currently and most likely, this customizer is needed only to permit requests to swagger docs in dev, stage
// profile.
authorizeCustomizer.invoke(this)
}
if (securityRealms == SecurityRealms.PROXY) {
// proxy-group rules
// As soon as CORS for /proxy/** is implemented manually inside ProxyServlet, we need to manually permit
// CORS requests to any proxied endpoint
authorize(ant(HttpMethod.OPTIONS, "/proxy/**"), permitAll)
authorize(ant(HttpMethod.POST, "/proxy/api/v1/courses/filtered"), hasAnyAuthority(NewdexPermissions.COURSE_EDITOR.name, NewdexPermissions.COURSE_VIEWER.name))
authorizeForEachMethod(
listOf(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE),
"/proxy/api/v1/courses/**",
hasAuthority(NewdexPermissions.COURSE_EDITOR.name)
)
authorizeForEachMethod(
listOf(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE),
"/proxy/api/v1/courses/*/chapters/**",
hasAuthority(NewdexPermissions.COURSE_EDITOR.name)
)
authorizeForEachMethod(
listOf(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE),
"/proxy/api/v1/courses/*/chapters/*/modules/**",
hasAuthority(NewdexPermissions.COURSE_EDITOR.name)
)
authorizeForEachMethod(
listOf(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE),
"/proxy/api/v1/courses/*/chapters/*/modules/*/lessons/**",
hasAuthority(NewdexPermissions.COURSE_EDITOR.name)
)
authorizeForEachMethod(
listOf(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE),
"/proxy/api/v1/lessons/*",
hasAuthority(NewdexPermissions.COURSE_EDITOR.name)
)
authorize(ant(HttpMethod.POST, "/proxy/api/v1/lessons"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(ant(HttpMethod.POST, "/proxy/api/v1/lessons/*/publish"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(ant(HttpMethod.GET, "/proxy/api/v1/lessons/*/versions"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(ant(HttpMethod.DELETE, "/v1/lessons/*/blocks/*"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(ant(HttpMethod.POST, "/v1/lessons/*/versions/*/restore"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(ant(HttpMethod.POST, "/proxy/api/v1/images"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(ant(HttpMethod.GET, "/proxy/api/v1/images/*"), hasAuthority(NewdexPermissions.COURSE_VIEWER.name))
authorizeForEachMethod(
listOf(HttpMethod.GET, HttpMethod.POST),
"/proxy/api/v1/courses/*/versions",
hasAuthority(NewdexPermissions.COURSE_EDITOR.name),
)
authorizeForEachMethod(
listOf(HttpMethod.PUT, HttpMethod.DELETE),
"/proxy/api/v1/courses/*/versions/*",
hasAuthority(NewdexPermissions.COURSE_EDITOR.name),
)
authorize(ant(HttpMethod.GET, "/proxy/api/v1/courses/*/chapters/*/versions"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(ant(HttpMethod.POST, "/proxy/api/v1/courses/*/chapters/*/versions/apply"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(ant(HttpMethod.GET, "/proxy/api/v1/courses/*/chapters/*/modules/*/versions"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(ant(HttpMethod.POST, "/proxy/api/v1/courses/*/chapters/*/modules/*/versions/apply"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(ant(HttpMethod.PUT, "/proxy/api/v1/grading-policies/*"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(ant(HttpMethod.GET, "/proxy/api/v1/grading-policies/by-course/*"), hasAnyAuthority(NewdexPermissions.COURSE_VIEWER.name))
authorize(ant(HttpMethod.POST, "/proxy/api/v1/grading-policies/*/module-policies/filtered"), hasAnyAuthority(NewdexPermissions.COURSE_EDITOR.name, NewdexPermissions.COURSE_VIEWER.name))
authorize(ant(HttpMethod.POST, "/proxy/api/v1/grading-policies/*/task-policies/filtered"), hasAnyAuthority(NewdexPermissions.COURSE_EDITOR.name, NewdexPermissions.COURSE_VIEWER.name))
authorize(ant(HttpMethod.POST, "/proxy/api/v1/programs/*"), hasAuthority(NewdexPermissions.PROGRAM_EDITOR.name))
authorize(ant(HttpMethod.PUT, "/proxy/api/v1/programs/*"), hasAuthority(NewdexPermissions.PROGRAM_EDITOR.name))
authorize(ant(HttpMethod.DELETE, "/proxy/api/v1/programs/*"), hasAuthority(NewdexPermissions.PROGRAM_EDITOR.name))
authorize(ant(HttpMethod.PATCH, "/proxy/api/v1/groups/*/grading-policies"), hasAuthority(NewdexPermissions.PROGRAM_EDITOR.name))
authorize(ant(HttpMethod.POST, "/proxy/api/v1/task-grades/reset-attempts"), hasAuthority(NewdexPermissions.INSTRUCTOR.name))
authorize(ant(HttpMethod.PUT, "/proxy/api/v1/task-grades/*"), hasAuthority(NewdexPermissions.INSTRUCTOR.name))
authorize(ant(HttpMethod.GET, "/proxy/api/v1/task-grades/changes/by-course/*/by-students"), hasAnyAuthority(NewdexPermissions.INSTRUCTOR.name, NewdexPermissions.COURSE_LISTENER.name))
authorize(ant(HttpMethod.POST, "/proxy/api/v1/groups/filtered"), hasAuthority(NewdexPermissions.INSTRUCTOR.name))
authorize(ant(HttpMethod.POST, "/proxy/api/v1/groups"), hasAuthority(NewdexPermissions.GROUP_EDITOR.name))
authorize(ant(HttpMethod.POST, "/proxy/api/v1/groups/*/start"), hasAuthority(NewdexPermissions.GROUP_EDITOR.name))
authorize(ant(HttpMethod.PUT, "/proxy/api/v1/groups/*"), hasAuthority(NewdexPermissions.GROUP_EDITOR.name))
authorize(ant(HttpMethod.DELETE, "/proxy/api/v1/groups/*"), hasAuthority(NewdexPermissions.GROUP_EDITOR.name))
authorize(ant(HttpMethod.PATCH, "/proxy/api/v1/groups/{id}/schedule"), hasAuthority(NewdexPermissions.INSTRUCTOR.name))
authorize(ant(HttpMethod.POST, "/proxy/api/v1/lesson-version-bindings/reset"), hasAuthority(NewdexPermissions.COURSE_LISTENER.name))
authorize(ant(HttpMethod.POST, "/proxy/api/v1/courses/authors"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(ant(HttpMethod.PUT, "/proxy/api/v1/courses/authors/*"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(ant(HttpMethod.DELETE, "/proxy/api/v1/courses/authors/*"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(ant(HttpMethod.PUT, "/proxy/api/v1/courses/*/authors"), hasAuthority(NewdexPermissions.COURSE_EDITOR.name))
authorize(ant(HttpMethod.GET, "/proxy/api/v1/courses/*/authors"), hasAuthority(NewdexPermissions.COURSE_VIEWER.name))
authorize(ant(HttpMethod.GET, "/proxy/api/v1/courses/authors/*"), hasAuthority(NewdexPermissions.COURSE_VIEWER.name))
authorize(ant(HttpMethod.GET, "/proxy/api/v1/courses/authors/filtered"), hasAuthority(NewdexPermissions.COURSE_VIEWER.name))
// Currently and most likely, this customizer is needed only to permit requests to openapi docs in dev, stage
// profile.
authorizeCustomizer.invoke(this)
}
// decided not to use denyAll on anyRequest to prevent situations when a client requests
// not-existed endpoint and gets 403 exception instead of 404
authorize(anyRequest, authenticated)
}
oauth2ResourceServer {
jwt {
jwkSetUri = keycloakProperties.getJwksUrl()
// We need custom JwtGrantedAuthoritiesConverter, because the default one use 'scopes' claim.
// Also, we can't use JwtGrantedAuthoritiesConverter with custom authoritiesClaimName, because
// Keycloak use complicated RealmAccess object as a value for 'realm_access' claim where it puts
// user's roles.
jwtAuthenticationConverter = JwtAuthenticationConverter()
.apply {
setJwtGrantedAuthoritiesConverter(
KeycloakJwtGrantedAuthoritiesConverter()
)
}
}
}
// for dev profile we use API_KEY authorization schema to make it possible to use REST API from
// Postman and from automated test scripts
if (devSecurityProperties != null) {
addFilterBefore<BearerTokenAuthenticationFilter>(DevApiKeySecurityFilter(devSecurityProperties))
}
}
return http.build()
}
private fun AuthorizeHttpRequestsDsl.authorizeForEachMethod(
methods: List<HttpMethod>,
pattern: String,
access: AuthorizationManager<RequestAuthorizationContext>,
interceptor: HandlerMappingIntrospector? = null,
) {
methods.forEach {
if (interceptor == null) {
authorize(ant(it, pattern), access)
} else {
authorize(mvc(interceptor, it, pattern), access)
}
}
}
private fun mvc(
interceptor: HandlerMappingIntrospector,
pattern: String,
): RequestMatcher = MvcRequestMatcher.Builder(interceptor)
.servletPath(SecurityRealms.API.servletPath)
.pattern(pattern)
private fun mvc(
interceptor: HandlerMappingIntrospector,
method: HttpMethod,
pattern: String
): RequestMatcher = MvcRequestMatcher.Builder(interceptor)
.servletPath(SecurityRealms.API.servletPath)
.pattern(method, pattern)
private fun ant(
pattern: String,
): RequestMatcher = AntPathRequestMatcher.antMatcher(pattern)
private fun ant(
method: HttpMethod,
pattern: String,
): RequestMatcher = AntPathRequestMatcher.antMatcher(method, pattern)
Как видно из сниппета, основные два пункта:
-
Указать base url для секурити фильтров при помощи securityMatcher
-
Указывать правила авторизации, используя MvcRequestMatcher и AntPathRequestMatcher.
Заключение
В статье был рассмотрен вопрос использования двух и более сервлетов совместно с Dispatcher servlet из состава Spring Web MVC.