null

Реализация своего сервлета рядом с Dispatcher Servlet

Введение

Очень редко, но иногда бывает необходимо использовать дополнительный сервлет рядом со стандартным 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)

Как видно из сниппета, основные два пункта:

  1. Указать base url для секурити фильтров при помощи securityMatcher

  2. Указывать правила авторизации, используя MvcRequestMatcher и AntPathRequestMatcher.

Заключение

В статье был рассмотрен вопрос использования двух и более сервлетов совместно с Dispatcher servlet из состава Spring Web MVC.

Next

Коротко о себе:

Пью кофе в компании Tune-it.

Java\Kotlin Backend Developer и немножко менеджер

Nothing has been found. n is 0