Delegating HTTP request handling from a Spring MVC Controller to Jersey 2 - spring-mvc

I'm trying to plug my set of RESTful endpoints (which rely on Jersey 2 JAX-RS implementation) into a 3rd party Spring MVC web application running on top of Tomcat 9.
I'm not allowed to modify web.xml, nor is my plug-in available when the container starts, so annotation-based configuration is not an option, either. This means I can't deploy Jersey's ServletContainer directly.
What I am allowed is plugging into Spring MVC, namely
creating a custom partial Spring application context and
implementing a custom Controller, so I have written this naïve implementation which is actually close to the standard ServletWrappingController:
package com.example
import org.glassfish.jersey.server.ResourceConfig
import org.glassfish.jersey.servlet.ServletContainer
import org.springframework.web.servlet.ModelAndView
import org.springframework.web.servlet.mvc.AbstractController
import java.io.IOException
import java.util.Collections
import java.util.Enumeration
import javax.servlet.ServletConfig
import javax.servlet.ServletContext
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
class JerseyServletInitializer(servletContext: ServletContext,
componentClasses: List<Class<*>>,
components: List<Any>): AbstractController() {
private val servletContainer: ServletContainer
init {
val resourceConfig = ResourceConfig()
.apply {
// Register org.glassfish.jersey.media.multipart.MultiPartFeature
componentClasses.forEach { clazz ->
register(clazz)
}
// Register REST endpoints
components.forEach { component ->
register(component)
}
}
// Provide a dummy ServletConfig
val servletConfig = object : ServletConfig {
override fun getInitParameter(name: String): String? = null
override fun getInitParameterNames(): Enumeration<String> = emptyList<String>().let {
Collections.enumeration(it)
}
override fun getServletName(): String = ServletContainer::class.java.name
override fun getServletContext(): ServletContext = servletContext
}
// Create and initialize Jersey's ServletContainer
servletContainer = ServletContainer(resourceConfig).apply {
init(servletConfig)
}
setSupportedMethods("GET", "POST", "PUT", "OPTIONS")
}
#Throws(IOException::class)
override fun handleRequestInternal(request: HttpServletRequest,
response: HttpServletResponse): ModelAndView? {
// Delegate all HTTP requests to Jersey
servletContainer.service(request, response)
return null
}
}
The above works fine with the only exception of file upload scenario (i. e. HTTP POST with Content-Type: multipart/form-data).
Spring detects such requests and converts a regular HttpServletRequest into a MultipartHttpServletRequest, reading out and exhausting its input stream (mark()/reset() are not supported). The web application has a custom MultipartResolver which I can't affect in any way:
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/>
This means Jersey receives a multipart HTTP POST with an empty request body, throws a MIMEParsingException and responds with HTTP 400 Bad Request.
As far as I understand, multipart resolution can only be disabled globally, and not on a per-controller basis (1, 2, 3).
Questions:
What is the best way to work the MultipartResolver around, so that Jersey receives an non-modified request?
Alternatively, can you recommend an API to re-construct the request body (i. e. do the opposite of what MultipartResolver does and feed the new HttpServletRequest to Jersey)? This can, of course, be done by hand, but I'd rather rely on the existing libraries.

Related

How to filter out headers during HTTP redirect in Camel?

In my Camel SpringBoot app I send a request (which includes a number of specific HTTP headers) and receive the 302 from the remote service. Camel automatically follows the redirect but includes all headers previously set, which causes some problems on the other side. Is there any way to control this behavior?
The most obvious is to remove specific headers in the Camel route with
.removeHeaders("Camel*")
This of course must be done for every header pattern and also multiple times if you have multiple outbound connections.
But Camel also has the concept of a HeaderFilterStrategy. Most components have a default implementation that is used by default.
In the case of HTTP this is HttpHeaderFilterStrategy. It is applied for all HTTP connections and filters the following headers:
content-length
content-type
host
cache-control
connection
date
pragma
trailer
transfer-encoding
upgrade
via
warning
Camel*
org.apache.camel*
You are free to implement your own custom HeaderFilterStrategy or extend one of Camel. You can then register it as a Spring bean and configure your endpoints to use it where needed.
.to("http:myEndpoint?&headerFilterStrategy=#myHeaderFilter") // myHeaderFilter = name of the Spring bean
I was able to solve this by creating a custom HTTP configurer which adds an interceptor to the request, e.g.
public class RedirectHttpClientConfigurer implements HttpClientConfigurer {
#Override
public void configureHttpClient(HttpClientBuilder clientBuilder) {
clientBuilder.addInterceptorFirst(new RedirectInterceptor());
}
public class RedirectInterceptor implements HttpRequestInterceptor {
#Override
public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
Object httpResponseContextAttr = context.getAttribute("http.response");
if (httpResponseContextAttr != null) {
HttpResponse httpResponse = (HttpResponse) httpResponseContextAttr;
int statusCode = httpResponse.getStatusLine().getStatusCode();
if (statusCode > 299 && statusCode < 399) {
request.removeHeaders("Authorization");
}
}
}
}
}
Then I configured it as a bean:
<spring:bean id="redirectConfigurer" class="com.foo.bar.RedirectHttpClientConfigurer"/>
and referenced it in the endpoint:
<to uri="http://com.foo.bar?httpClientConfigurer=#redirectConfigurer"/>

Initialising OAuth WebClient Bean in Spring MVC

I have a WebApp JSP project deployed on Weblogic 12 as a WAR.
My gradle build includes mvc and webflux:
implementation 'org.springframework.boot:spring-boot-starter-web:2.3.2.RELEASE'
implementation ("org.springframework.boot:spring-boot-starter-security:2.3.2.RELEASE")
implementation ("org.springframework.boot:spring-boot-starter-oauth2-client:2.3.2.RELEASE")
implementation ("org.springframework.boot:spring-boot-starter-webflux:2.3.2.RELEASE")
I am trying to configure OAuth2 to use client_credentials flow from my client JSP application.
I need the #Controller class to use WebClient and propagate the access token to a Resource Server.
My Bean to create the WebClient is seen below.
#Bean
public ReactiveClientRegistrationRepository getRegistration() {
ClientRegistration registration = ClientRegistration
.withRegistrationId("ei-gateway")
.tokenUri("https://xxxxx.xxxxxxx.net/auth/oauth/v2/token")
.clientId("xxx-xxxx-43e9-a407-xxxxx")
.clientSecret("xxxxxx-3d21-4905-b6e5-xxxxxxxxxx")
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.build();
return new InMemoryReactiveClientRegistrationRepository(registration);
}
#Bean
public WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations, ServerOAuth2AuthorizedClientRepository authorizedClients) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients);
oauth.setDefaultOAuth2AuthorizedClient(true);
return WebClient.builder()
.filter(oauth)
.defaultHeader("accept", "application/json")
.defaultHeader("content-type", "application/json")
.defaultHeader("environment", environment)
.filter(logRequest())
.filter(logResponse())
.build();
}
However I get the following error during compile:
Could not autowire. There is more than one bean of 'ReactiveClientRegistrationRepository' type.
Beans:
clientRegistrationRepository   (ReactiveOAuth2ClientConfigurations.class)
getRegistration   (WebSecurityConfiguration.java)
However when I uncomment out the getRegistration Bean method and configure the oauth client registration via the web.xml, then when deploying the application I get this error:
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}:org.springframework.beans.factory.NoSuchBeanDefinitionException:No qualifying bean of type 'org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
I see from the ReactiveOAuth2ClientAutoConfiguration source code that the Reactive OAuth2 Auto Configuration is not run when ReactiveOAuth2ClientAutoConfiguration.NonServletApplicationCondition is set.
#Configuration(proxyBeanMethods = false)
#AutoConfigureBefore(ReactiveSecurityAutoConfiguration.class)
#EnableConfigurationProperties(OAuth2ClientProperties.class)
#Conditional(ReactiveOAuth2ClientAutoConfiguration.NonServletApplicationCondition.class)
#ConditionalOnClass({ Flux.class, EnableWebFluxSecurity.class, ClientRegistration.class })
#Import({ ReactiveOAuth2ClientConfigurations.ReactiveClientRegistrationRepositoryConfiguration.class,
ReactiveOAuth2ClientConfigurations.ReactiveOAuth2ClientConfiguration.class })
public class ReactiveOAuth2ClientAutoConfiguration {
}
Can anyone suggest a course of action? Is is possible to manually configure the ReactiveOAuth2ClientConfiguration?
Thanks
Form what I understand ReactiveClientRegistrationRepository is not available since you are not using a reactive stack, and here's how you can set up WebClient to be used in a Servlet environment.
Setup application properties so Spring autowires ClientRegistrationRepository and OAuth2AuthorizedClientRepository for you.
spring.security.oauth2.client.provider.my-oauth-provider.token-uri=https://xxxxx.xxxxxxx.net/auth/oauth/v2/token
spring.security.oauth2.client.registration.ei-gateway.client-id=xxx-xxxx-43e9-a407-xxxxx
spring.security.oauth2.client.registration.ei-gateway.client-xxxxxx-3d21-4905-b6e5-xxxxxxxxxx
spring.security.oauth2.client.registration.ei-gateway.provider=my-oauth-provider
spring.security.oauth2.client.registration.ei-gateway.scope=read,write
spring.security.oauth2.client.registration.ei-gateway.authorization-grant-type=client_credentials
Setup configuration to indicate that your application needs to act as an oauth2 Client
#EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2Client();
}
}
Expose WebClient bean configured to use client credentials
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
#Bean
WebClient webClient(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(
oAuth2AuthorizedClientManager);
// default registrationId - Only if you are not using the webClient to talk to different external APIs
oauth2Client.setDefaultClientRegistrationId("ei-gateway");
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
Now you can use WebClient in your code to access external protected resources.
references:
https://docs.spring.io/spring-security/site/docs/current/reference/html5/#oauth2client
https://docs.spring.io/spring-security/site/docs/current/reference/html5/#oauth2Client-webclient-servlet
https://docs.spring.io/spring-security/site/docs/current/reference/html5/#defaulting-the-authorized-client
This set up worked for me when the application is not configured as a resource server, I had to use a different configuration when the application needs to use WebClient, but also configured to be a resource server.

Spring Boot - MockMVC forwardedUrl using Thymeleaf

I have a basic SpringBoot app. using Spring Initializer, embedded Tomcat, Thymeleaf template engine, and package as an executable JAR file.
I have this controller:
#Controller
#RequestMapping("/deviceevent")
public class DeviceEventController {
#RequestMapping(value={ "/list"}, method = { RequestMethod.GET})
public String deviceeventList() {
return "tdk/deviceEvent/DeviceEventList";
}
}
and this other test class. Tests using Spring's MockMVC framework. This drives an MVC application in a test, as if it was running in a container,
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration
#WebMvcTest
public class MockMvcTests {
// Pull in the application context created by #ContextConfiguration
#Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
#Before
public void setup() {
// Setup MockMVC to use our Spring Configuration
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
#Test
public void getDeviceEventsTest() throws Exception {
this.mockMvc
.perform(get("/deviceevent/list") //
.accept(MediaType.parseMediaType("text/html;charset=UTF-8")))
.andExpect(status().isOk()) //
.andExpect(model().size(1)) //
.andExpect(forwardedUrl("tdk/deviceEvent/DeviceEventList"));
}
But I got this error in the forwarded URL. I always used this method in JSP, never in Thymeleaf, but I guess that It is the same:
java.lang.AssertionError: Forwarded URL expected:</tdk/deviceEvent/DeviceEventList> but was:<null>
Assuming a standard Thymeleaf/Spring setup, it looks like there is a misunderstanding with what the controller is doing - when the controller returns that string "tdk/deviceEvent/DeviceEventList" it is not forwarding the HTTP request somewhere, but returning a view name.
With a normal Spring-thymeleaf setup, that string corresponds to the name of a thymeleaf view that will be rendered on hitting that endpoint (I assume the controller is just serving a normal webpage - so that path probably corresponds to some file path most likely in src/main/resources - but again, this depends a lot on your spring config) - at this point the HTTP request has not been returned to the user, and Spring is still processing it - and will attempt to render the HTML view before returning to the user.
The forwarded URL is used if Spring is not rendering anything but instead returning a HTTP response to the user to forward them to another URL (which will start a different Spring request-response process) using a 301/302 mechanism.
Note the difference in the following methods:
#RequestMapping( value="/document", method=RequestMethod.GET )
public String newDocumentSettings( Model model ){
model.addAllAttributes( contentManagementService.documentToJson() );
return "pages/document-settings";
}
#RequestMapping( value="/document", method=RequestMethod.POST )
public String createNewDocument( #RequestParam String title, #RequestParam String overview, #RequestParam String tags ){
Document doc = documentService.createDocument( title, overview, tags );
return "redirect:/document/${doc.url}/1?getting-started";
}
The first renders the template at the given filepath, the second returns a redirect command to the browser to make another HTTP request to the given URL.
In any case, the forwardedUrl in your test case is because hte HTTP Response doesn't have a URL to forward to (because its returning the HTML). If you do want forwarding behaviour (e.g. you actually want to complete the response and the browser to make a second HTTP request) then you would likely need to update the controller as per example, however, if you are happy with the rendered html page, then the test is invalid (look at the Thymeleaf testing framework to see how to test templating).
Caveat: This is based on the assumption of default Spring-Boot config - if you have other config whereby that string does result in a forwarded HTTP request then this doesnt apply!
Taking a guess here, but the URL tdk/deviceEvent/DeviceEventList is probably not defined. Try replacing it with the URL associated with your context (edit as necessary):
#Test
public void getDeviceEventsTest() throws Exception {
this.mockMvc
.perform(get("/deviceevent/list")
.accept(MediaType.parseMediaType("text/html;charset=UTF-8")))
.andExpect(status().isOk())
.andExpect(model().size(1))
.andExpect(forwardedUrl("/WEB-INF/tdk/deviceEvent/DeviceEventList.html"));
}
Aside, instead of:
#RequestMapping(value={ "/list"}, method = { RequestMethod.GET})
you can use the shorthand:
#GetMapping("/list")

Spring Cloud Netflix : Passing host request parameter via RequestInterceptor to FeignClient

I am building a Spring Cloud project (Brixton.M4 with Spring Boot 1.3.1) with Eureka, Zuul and FeignClient where I am trying to add multi tenancy support (Tenants are identified by subdomain : tenant1.myservice.com). To do so, I would like to somehow pass the original subdomain along requests that are forwarded from a service to the other via Feign but I can't seem to be able to find the right way to do it.
What I have is a client that exposes a #RestController which calls a #FeignClient to communicate with my backend which exposes server operations to the client through its own #RestController.
The #FeignClient using same interface as my #RestController on the server :
#FeignClient(name = "product")
public interface ProductService extends IProductService {
}
What I am currently trying to do is set a header in a RequestInterceptor :
#Component
public class MultiTenancyRequestInterceptor implements RequestInterceptor {
private CurrentTenantProvider currentTenantProvider;
#Autowired
public MultiTenancyRequestInterceptor(CurrentTenantProvider currentTenantProvider) {
this.currentTenantProvider = currentTenantProvider;
}
#Override
public void apply(RequestTemplate template) {
try {
template.header("TENANT", currentTenantProvider.getTenant());
} catch (Exception e) {
// "oops"
}
}
}
My provider class is a simple component where I'm trying to inject a request / session scope bean :
#Component
public class CurrentTenantProvider {
#Autowired
private CurrentTenant currentTenant;
//...
}
The bean (I tried both session and request scope) :
#Bean
#Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public CurrentTenant currentTenant() {
return new CurrentTenant();
}
On the server, I use Hibernate multitenant provider that is supposed to catch the header value and use it to define which DB to connect to :
#Autowired
private HttpServletRequest httpRequest;
#Override
public String resolveCurrentTenantIdentifier() {
return httpRequest.getHeader("TENANT");
}
It seems the Feign call to the server is done in another thread and out of the incoming request scope, so i'm not sure how to pass that value along.
It all works fine when I hardcode the tenant value in the RequestInterceptor so I know the rest is working properly.
I have also looked at many other posts about Zuul "X-Forwaded-For" header and cannot find it in the request received on the server. I have also tried adding a ZuulFilter to pass host name to next request but what I see is that original request to the Client is picked up by the ZuulFilter and I can add but not when the Feign request is sent to the backend service even if I map it in zuul (i guess that is intended ?).
I am not really sure what's the next step and would appreciate some suggestions.
Hope that it's of any use for you but we're doing sth similar in Spring-Cloud-Sleuth but we're using a ThreadLocal to pass span between different libraries and approaches (including Feign + Hystrix).
Here is an example with the highlighted line where we retrieve the Span from the thread local: https://github.com/spring-cloud/spring-cloud-sleuth/blob/master/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/TraceFeignClientAutoConfiguration.java#L123

Is Spring Boot + Spring MVC + Ratpack possible?

We're a Spring Boot shop and rely heavily on Spring MVC for our REST endpoints. We use Boot and embedded Tomcat to create a self-hosting JAR. Is it possible to replace Tomcat with Ratback while still keeping all my Spring MVC code in place? I am afraid that Spring MVC is tied into the servlet specification somehow and will not run without a servlet container. I am aware of dsyer/spring-boot-ratpack work but after skimming the code couldn't decide if Spring MVC would play well using the bridge. Is anyone aware of any work that will allow us to retain our investment in Spring MVC and have Spring Boot use Ratpack to manage HTTP traffic?
I suspect the crux of your question can be distilled to: "can we put our Spring controllers on top of Ratpack's non-blocking HTTP layer?" and the simplest answer to that question is no, for reason that the MVC programming model doesn't fit well into the reactive/NIO model very well.
However, if your application has followed some common model-view-controller-(and service) patterns, then your controllers should really just be performing data binding and parsing and delegating out to a service layer. If that's the case, then likely the code in your controller is already non-blocking, and you could easily translate it to Ratpack code.
As an example, consider the following #RestController in a Spring Boot app:
#RestController
#RequestMapping("/user")
class UserController {
#Autowired
UserService userService
#RequestMapping(method = RequestMethod.POST)
Long create(#RequestBody #Valid User user) {
User savedUser = userService.save(user)
return savedUser.id
}
}
Spring's data binding aspect is a computation process (ie isn't I/O bound), so we can easily translate this into a Ratpack handler:
import app.SpringConfig
import app.User
import app.UserService
import org.springframework.boot.SpringApplication
import org.springframework.context.ApplicationContext
import ratpack.jackson.JacksonModule
import static ratpack.groovy.Groovy.ratpack
import static ratpack.jackson.Jackson.fromJson
import static ratpack.jackson.Jackson.json
import static ratpack.spring.Spring.spring
ratpack {
bindings {
add(new JacksonModule())
bindInstance(ApplicationContext, SpringApplication.run(SpringConfig))
}
handlers { ApplicationContext ctx ->
register(spring(ctx))
prefix("user") {
handler { UserService userService ->
byMethod {
post {
def user = parse(fromJson(User))
blocking {
userService.save(user)
} then { User savedUser ->
render(json(savedUser))
}
}
}
}
}
}
}
Where SpringConfig looks like this:
package app
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
#Configuration
class SpringConfig {
#Bean
UserService userService() {
new UserService()
}
}
And here's a functional test to prove it:
package app
import com.fasterxml.jackson.databind.ObjectMapper
import ratpack.groovy.test.GroovyRatpackMainApplicationUnderTest
import ratpack.test.ApplicationUnderTest
import ratpack.test.http.TestHttpClient
import spock.lang.Shared
import spock.lang.Specification
import static groovy.json.JsonOutput.toJson
class FuncSpec extends Specification {
#Shared ApplicationUnderTest aut = new GroovyRatpackMainApplicationUnderTest()
#Shared ObjectMapper mapper = new ObjectMapper()
#Delegate TestHttpClient client = aut.httpClient
def "should parse and save user"() {
given:
def user = new User(username: "dan", email: "danielpwoods#gmail.com")
when:
requestSpec { spec ->
spec.body { b ->
b.type("application/json")
b.text(toJson(user))
}
}
post('user')
then:
def savedUser = mapper.readValue(response.body.text, User)
and:
savedUser.id
}
}
Hope this helps!
The Spring MVC programming model is not very heavily dependent on Servlet APIs, but it's not supported in any other containers (i.e. not in Ratpack). There is some async stuff there now and Servlet 3.1 enhances it some more, so if that's the part of Ratpack that attracts you, maybe just using that would be a better approach. You won't get all the way to reactive and non-blocking IO that way though.

Resources