Apache Shiro is breaking the CORS configuration of my Spring Boot RestAPI - spring-boot

After tinkering with different security frameworks, I've decided to go with Apache Shiro for my Spring Boot RestAPI, because it appears to offer the necessary flexibility without too much bureaucratic overhead. So far, I haven't done anything except adding the maven dependency to my project:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.5.1</version>
</dependency>
This forced me to define a Realm bean in order to get the application started:
#Bean
public Realm realm() {
return new TMTRealm();
}
The bean pretty much does nothing yet, except for implementing the Realm interface:
public class TMTRealm implements Realm {
private static final String Realm_Name = "realm_name";
#Override
public String getName() {
return Realm_Name;
}
#Override
public boolean supports(AuthenticationToken token) {
return false;
}
#Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
return null;
}
}
So far so good. Except that now my RestAPI is violating CORS policy by not adding the 'Access-Control-Allow-Origin' header to any of its responses. I've noticed that Chrome doesn't send any dedicated OPTIONS request but two requests of the same method, GET in this case, with the first one failing as follows:
Access to XMLHttpRequest at 'http://localhost:8081/geo/country/names?typed=D&lang=en-US' from origin 'http://localhost:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Without Shiro present it works perfectly fine, both the elegant way of using Spring's #CrossOrigin annotation on the controller and the brute force 'old school' way of defining a CorsFilter bean:
#Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOrigins(Arrays.asList("*"));
config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowedMethods(Arrays.asList("OPTIONS", "GET", "POST", "PUT", "DELETE"));
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
I've implemented Spring's ApplicationListener interface to hook into when the ApplicationContext is started and can thus see that the corsFilter bean is registered and present:
#Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("############ - application context started with beans: ");
final String[] beans = event.getApplicationContext().getBeanDefinitionNames();
Arrays.parallelSort(beans);
for (final String bean : beans) {
System.out.println(bean);
}
}
Output:
...
conditionEvaluationDeltaLoggingListener
conventionErrorViewResolver
corsFilter
countryController
...
But the filter is never called upon any request (I've set a breakpoint and System.out to prove it). I've also noticed that there are three Shiro beans present:
shiroEventBusAwareBeanPostProcessor
shiroFilterChainDefinition
shiroFilterFactoryBean
Therefore, I assume that probably the shiroFilterFactoryBean is breaking it somehow and needs extra attention and configuration. Unfortunately the Apache Shiro documentation doesn't seem to say anything about cross-origin requests, and I would assume that this is not (necessarily) part of Shiro's security concerns but rather of the underlying Restful API, that is Spring. Googling the issue didn't yield any helpful results either, so my suspicion is that I'm missing something big, or worse, something small and obvious here. While I'm trying to figure this out, any help or hint is greatly appreciated, thanks!

Awight, I figured it out. It's been a while that I was in filter-servlet land, so I didn't think of the order in which filters are executed. The naive way that I did it, the Shiro filter chain was always executed before my custom CorsFilter (and apparently the default Spring processor of #CrossOrigin annotation as well). Since I haven't yet configured Shiro yet, any request would be rejected as neither authenticated nor authorized, and so the CorsFilter was never executed causing a response without Access-Control-Allow-Origin header.
So, either I configure Shiro properly or just make sure to have the CorsFilter executed prior to the Shiro filter by using Spring's FilterRegistrationBean like this (setOrder to zero):
#Configuration
public class RestApiConfig {
#Bean
public FilterRegistrationBean<CorsFilter> corsFilterRegistrationBean() {
final FilterRegistrationBean<CorsFilter> registration = new FilterRegistrationBean<>(this.corsFilter());
registration.setOrder(0);
return registration;
}
private CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOrigins(Arrays.asList("*"));
config.setAllowedHeaders(Arrays.asList("*"));
config.setAllowedMethods(Arrays.asList("OPTIONS", "GET", "POST", "PUT", "DELETE"));
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
#Bean
public Realm realm() {
return new TMTRealm();
}
}
Edit:
D'uh, it was actually too easy for me to notice. All you have to do is to configure a ShiroFilterChainDefinition bean, so that it will refer to annotations on the controller classes or methods, like this:
#Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
final DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/**", "anon");
return chainDefinition;
}
Just like it is described in the documentation. Now it works both with a CorsFilter bean or with Spring's #CrossOrigin annotation. If there is no Shiro annotation present on the controller method, the request will be passed through.

Related

Enable CORS origin graphql

I'm working on graphQL and spring boot project. The API works well using graphiQL but when trying to consume it using Apollo vueJS, it causes CORS origin error.
I'm using #CrossOrigin annotation in ProductQuery class which implements GraphQLQueryResolver like below:
#CrossOrigin(origins = "https://localhost:8081")
public List<Product> getProducts(){return this.productService.findAll(); }
Here is the error displayed on frontEnd project:
I appreciate your help.
For local development you may need a CorsFilter bean to enable your local origin:
#Configuration
#Profile("local")
public class LocalCorsConfiguration {
#Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("http://localhost:3000");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/graphql/**", config);
return new CorsFilter(source);
}
}
Don't forget to start the application with -Dspring.profiles.active=local.
To solve this issue you need to add this in your application properties graphql.servlet.corsEnabled: true after that your server response header will have the CORS properties.
What worked for me was the solution explained in the official docs
My version of a configurer bean looks like this:
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
#Override
public void addCorsMappings(final CorsRegistry registry) {
registry.addMapping("/graphql/**")
.allowedOrigins(CorsConfiguration.ALL)
.allowedHeaders(CorsConfiguration.ALL)
.allowedMethods(CorsConfiguration.ALL);
}
};
}
Since Spring Boot 2.7.0 there are configuration properties for CORS with GraphQL:
spring:
graphql:
cors:
allow-credentials: true
allowed-origins:
- http://localhost:3000
See GraphQlCorsProperties.java for further properties.

Spring Boot CORS enable through configuration

I've read many posts regarding CORS in Spring (Boot) but none could answer my question, but maybe I just missed the answer, so bear with me.
I have a REST Webservice currently used only for server to server calls. I now want to open some endpoints to be called directly from the browser, but not from the same domain, thus CORS. I got it working for some endpoints by doing two things:
1. enabling OPTIONS in my WebSecurityConfigurerAdapter:
http.authorizeRequests()
.mvcMatchers(HttpMethod.OPTIONS,
"/endpont1",
"/endpoint2")
.permitAll()
2. adding the following annotation to my #GetMapping for these endpoints:
#CrossOrigin(origins = "${cors.origin}", allowCredentials = "true",
exposedHeaders = ResponseUtils.CONTENT_DISPOSITION)
#GetMapping("/endpoint1")
The problem is, as far as I understand the documentation, leaving origins empty allows CORS for any domain. And I don't want to allow OPTIONS if I don't need CORS.
What is the best way to make this configurable through a properties file?
The "embedded" application.properties should have it disabled, but if the tenant wants to enable it we can provide an additional application-tenant.properties where we could enable it for certain domains and start the application with the appropriate profile.
EDIT: I found an answer in another post which looks interesting and maybe I can do this conditionally:
https://stackoverflow.com/a/43559288/3737177
After a few try and errors I found a working solution based on this answer:
#Configuration
#EnableConfigurationProperties
#Order(1)
public class EndpointSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private RequestMatcher requestMatcher;
#Value("${cors.origins:}")
private String corsOrigins;
#Override
protected void configure(HttpSecurity http) throws Exception {
if (StringUtils.isNotBlank(corsOrigins)) {
http.cors().configurationSource(buildConfigurationSource());
}
http.requestMatchers().mvcMatchers("/endpoint1", "/pendpoint2")
.and().csrf().requireCsrfProtectionMatcher(requestMatcher)
.and().authorizeRequests().anyRequest()
.hasAnyRole(SecurityConfiguration.ROLE_ENDPOINT_USER, SecurityConfiguration.ROLE_ADMIN)
.and().httpBasic();
}
private CorsConfigurationSource buildConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList(corsOrigins.split(",")));
configuration.setAllowedMethods(Arrays.asList("GET");
configuration.setAllowedHeaders(Arrays.asList("authorization"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/endpoint1", configuration);
source.registerCorsConfiguration("/endpoint2", configuration);
return source;
}
}
If there is a cors.origins property in the application-tenant.properties, it enables CORS and configures the allowed methods and headers. CSRF is also enabled for same origin requests.
The truth is that you CANNOT set the global CORS configuration using the application.properties file. You HAVE TO use JavaConfig as described here.
implements WebMvcConfigurer
and override below method
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://domain4.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(false).maxAge(4200);
}
Or
Add below code snippet in Application.java
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/greeting-javaconfig").allowedOrigins("http://localhost:9000");
}
};
}
I think this way you can add properties in a property file and use those in code here and based on different flavors you can override those properties.
For loading custom properties files you can use
spring.config.import=optional:classpath:cors.yml
or from java args
-Dspring.config.import=optional:classpath:cors.yml
This method support reading from file, from url, repository and config server
Spring documentation about this
For reading CORS configuration from properties file you may use library (I'm developer of this)
<dependency>
<groupId>io.github.iruzhnikov</groupId>
<artifactId>spring-webmvc-cors-properties-autoconfigure</artifactId>
<version>VERSION</version>
</dependency>
and properties config
spring:
web:
cors:
enabled: true
mappings: #spring.web.cors.mappings.<any_name>.<property>: <value>
anyName: #just any name, just for grouping properties under the same path pattern (not used in internal logic)
paths: #ant style path pattern, ATTENTION! not ordered, /** pattern override all other pattern
- /path/to/api
- /path/to/api/**
#allowed-origins: "*"
allowed-methods: GET #Enable override all defaults! If disabled: a lot more from all the controller methods included from the path pattern matches
#allowed-headers: "*"
#exposed-headers: ('*' - not-supported)
#allow-credentials: true
allowed-origin-patterns: .*
#max-age: PT30M

Spring Security WebFlux and LDAP

What customization's are required in order to secure a Reactive Spring Boot application with LDAP? The examples I've seen so far are based on Spring MVC and the example for securing a WebFlux only shows a simple Reactive example with an in-memory Map.
Here is one solution for this that I have come up with and tested.
Deserving special attention is this information in this class: ReactiveAuthenticationManagerAdapter. There, it states:
Adapts an AuthenticationManager to the reactive APIs. This is somewhat
necessary because many of the ways that credentials are stored (i.e.
JDBC, LDAP, etc) do not have reactive implementations. What's more is
it is generally considered best practice to store passwords in a hash
that is intentionally slow which would block ever request from coming
in unless it was put on another thread.
First, create a configuration class. This will handle the connectivity to LDAP.
#Configuration
public class ReactiveLdapAuthenticationConfig {
// Set this in your application.properties, or hardcode if you want.
#Value("${spring.ldap.urls}")
private String ldapUrl;
#Bean
ReactiveAuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) {
BindAuthenticator ba = new BindAuthenticator(contextSource);
ba.setUserDnPatterns(new String[] { "cn={0},ou=people" } );
LdapAuthenticationProvider lap = new LdapAuthenticationProvider(ba);
AuthenticationManager am = new ProviderManager(Arrays.asList(lap));
return new ReactiveAuthenticationManagerAdapter(am);
}
#Bean
BaseLdapPathContextSource contextSource() {
LdapContextSource ctx = new LdapContextSource();
ctx.setUrl(ldapUrl);
ctx.afterPropertiesSet();
return ctx;
}
}
After that, you'll want to configure your security following the patterns here. The most basic chain configuration is about this:
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange()
.anyExchange().authenticated()
.and()
.httpBasic();
return http.build();
}
For completeness, you'll want to make sure you have these:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>
Other References
EnableReactiveMethodSecurity
BindAuthenticator
LdapAuthenticationProvider
ProviderManager
The above example didn't work for me using Windows Active Directory. I could get LDAP Authentication to work in stand-alone (non-Spring) Java, but the above solution always gave me error 52e (user known, but invalid password).
Following on from the example above, I used the same pom.xml and #EnableWebFluxSecurity ... SecurityWebFilterChain(...), but with the following;
#Configuration
public class ReactiveLdapAuthenticatoinConfig {
#Bean
ReactiveAuthenticationManager authenticationManager() {
ActiveDirectoryLdapAuthenticationProvider adlap =
new ActiveDirectoryLdapAuthenticationProvider(
"{my.domain}",
"ldap://{my.ldap.server}.{my.domain}"
);
AuthenticationManager am = new ProviderManager(Arrays.asList(adlap));
return new ReactiveAuthenticationManagerAdapter(am);
}
}
In order to return the signed-in user, one would use something like;
#GetMapping(value = '/user')
public Mono<String> getUser(Mono<Principal> principal) {
return principal.map(Principal::getName);
}

Spring #FeignClient with OAuth2FeignRequestInterceptor not working

I'm trying to set FeignClient with OAuth2 to implement "Relay Token". I just want FeignClient to relay / propagate the OAuth2 Token that comes from ZuulProxy (SSO Enabled).
I use Spring 1.3.1-RELEASE and Spring Cloud Brixton.M4.
I have added an interceptor in a custom #FeignClient configuration:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
import feign.RequestInterceptor;
#Configuration
public class FeignClientConfiguration {
#Value("${security.oauth2.client.userAuthorizationUri}")
private String authorizeUrl;
#Value("${security.oauth2.client.accessTokenUri}")
private String tokenUrl;
#Value("${security.oauth2.client.client-id}")
private String clientId;
// See https://github.com/spring-cloud/spring-cloud-netflix/issues/675
#Bean
public RequestInterceptor oauth2FeignRequestInterceptor(OAuth2ClientContext oauth2ClientContext){
return new OAuth2FeignRequestInterceptor(oauth2ClientContext, resource());
}
#Bean
protected OAuth2ProtectedResourceDetails resource() {
AuthorizationCodeResourceDetails resource = new AuthorizationCodeResourceDetails();
resource.setAccessTokenUri(tokenUrl);
resource.setUserAuthorizationUri(authorizeUrl);
resource.setClientId(clientId);
// TODO: Remove this harcode
resource.setClientSecret("secret");
return resource;
}
}
And I add the configuration to my #FeignClient like that:
#FeignClient(name = "car-service", configuration = FeignClientConfiguration.class)
interface CarClient {
#RequestMapping(value = "car-service/api/car", method = GET)
List<CarVO> getAllCars();
}
The application starts but when I use the Feign Client from my service I get:
2016-01-08 13:14:29.757 ERROR 3308 --- [nio-9081-exec-1] o.a.c.c.C.[.[.[. [dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in
context with path [/user-service] threw exception [Request processing failed; nested exception is com.netflix.hystrix.exception.HystrixRuntimeException: getAllCars failed and no fallback available.] with root cause
java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131) ~[spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
at org.springframework.web.context.request.AbstractRequestAttributesScope.get(AbstractRequestAttributesScope.java:41) ~[spring-web-4.2.4.RELEASE.jar:4.2.4.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:340) ~[spring-beans-4.2.4.RELEASE.jar:4.2.4.RELEASE]
I want my application / microservice (the one that uses the #FeingClient to call the other application / microservice) to be STATELESS. However, I have tried both, with security.sessions=STATELESS (SpringBoot default) and security.sessions=ALWAYS (just to try).
In both cases I got the same exception.
Having a look at the code I have seen that the OAuth2ClientContext is saved in Session (Session scoped bean). How does it work when you want to implement a STATELESS OAuth2 enabled application / microservice? Precisely this is one of the big advantages of using OAuth2 in my current scenario. However, as I said, the result was the same enabling sessions.
Can someone help with this, please?
Thanks so much! :-)
I have found out that the problem is that Hystrix forces code execution in another thread and so you have no access to request / session scoped beans.
I was using #FeignClient with Hystrix enabled. When I disable Hystrix using feign.hystrix.enabled: false
the call from Microservice A to Microservice B relaying the token (using OAuth2FeignRequestInterceptor) works fine.
However, it would be desirable to be able to keep Hystrix enabled.
I have seen there is a new module that improves Hystrix - Feign (feign-hystrix module) in this regard in this post:
Does Spring Cloud Feign client call execute inside hystrix command?
However, I don't see how to properly do the setup using feign-hystrix and I was not able to find an example. Please, could you help with this or provide an example using feign-hystrix?
Thanks so much!
I am not exactly sure if I understood you correctly but the following worked for me.
See https://jfconavarrete.wordpress.com/2014/09/15/make-spring-security-context-available-inside-a-hystrix-command/
Basically the tutorial shows how to setup / augment hystrix with an additional "plugin" so the security context is made available inside hystrix wrapped calls via a threadlocal variable
With this setup all you need to do is define a feign request interceptor like so:
#Bean
public RequestInterceptor requestTokenBearerInterceptor() {
return new RequestInterceptor() {
#Override
public void apply(RequestTemplate requestTemplate) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
requestTemplate.header("Authorization", "Bearer " + details.getTokenValue());
}
};
}
With this setup the token contained in the request is made available to the feign request interceptor so you can set the Authorization header on the feign request with the token from your authenticated user.
Also note that with this approach you can keep your SessionManagementStrategy "STATELESS" as no data has to be "stored" on the server side
USE THIS CODE AND COMMENT RESTEMPLATE config when you are using as ribbon client instead of that here we will use oauth2restTemplate
#EnableOAuth2Client
#Configuration
public class OAuthClientConfig {
#Value("${config.oauth2.accessTokenUri}")
private String tokenUri;
#Value("${app.client.id}")
private String clientId;
#Value("${app.client.secret}")
private String clientSecret;
#Bean
protected OAuth2ProtectedResourceDetails resource() {
ResourceOwnerPasswordResourceDetails resource;
resource = new ResourceOwnerPasswordResourceDetails();
List<String> scopes = new ArrayList<String>(2);
scopes.add("write");
scopes.add("read");
resource.setAccessTokenUri(tokenUri);
resource.setClientId(clientId);
resource.setClientSecret(clientSecret);
resource.setGrantType("password");
resource.setScope(scopes);
return resource;
}
#Bean
public OAuth2ClientContext oauth2ClientContext() {
DefaultOAuth2ClientContext defaultOAuth2ClientContext = new DefaultOAuth2ClientContext();
return defaultOAuth2ClientContext;
}
#Bean
#Primary
#LoadBalanced
public OAuth2RestTemplate oAuth2RestTemplate(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails,
OAuth2ClientContext oauth2ClientContext) {
OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(oAuth2ProtectedResourceDetails, oauth2ClientContext);
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
restTemplate.setRequestFactory(factory);
return restTemplate;
}
#Bean
public OAuth2FeignRequestInterceptor aauthRequestInterceptor(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails,
OAuth2ClientContext oauth2ClientContext)
{
OAuth2FeignRequestInterceptor auth2FeignRequestInterceptor=new OAuth2FeignRequestInterceptor(oauth2ClientContext, oAuth2ProtectedResourceDetails);
return auth2FeignRequestInterceptor;
}

Spring Boot with Two MVC Configurations

I have a Spring Boot app with a REST API, using Jackson for the JSON view configuration. It works great and I can get all the Spring Boot goodness.
However, I need to add an additional REST API that is similar but with different settings. For example, among other things, it needs a different Jackson object mapper configuration because the JSON will look quite a bit different (e.g. no JSON arrays). That is just one example but there are quite a few differences. Each API has a different context (e.g. /api/current and /api/legacy).
Ideally I'd like two MVC configs mapped to these different contexts, and not have to give up any of the automatic wiring of things in boot.
So far all I've been able to get close on is using two dispatcher servlets each with its own MVC config, but that results in Boot dropping a whole bunch of things I get automatically and basically defeats the reason for using boot.
I cannot break the app up into multiple apps.
The answer "you cannot do this with Boot and still get all its magic" is an acceptable answer. Seems like it should be able to handle this though.
There's several ways to achieve this. Based on your requirement , Id say this is a case of managing REST API versions.
There's several ways to version the REST API, some the popular ones being version urls and other techniques mentioned in the links of the comments.
The URL Based approach is more driven towards having multiple versions of the address:
For example
For V1 :
/path/v1/resource
and V2 :
/path/v2/resource
These will resolve to 2 different methods in the Spring MVC Controller bean, to which the calls get delegated.
The other option to resolve the versions of the API is to use the headers, this way there is only URL, multiple methods based on the version.
For example:
/path/resource
HEADER:
X-API-Version: 1.0
HEADER:
X-API-Version: 2.0
This will also resolve in two separate operations on the controller.
Now these are the strategies based on which multiple rest versions can be handled.
The above approaches are explained well in the following: git example
Note: The above is a spring boot application.
The commonality in both these approaches is that there will need to be different POJOS based on which Jackson JSON library to automatically marshal instances of the specified type into JSON.
I.e. Assuming that the code uses the #RestController [org.springframework.web.bind.annotation.RestController]
Now if your requirement is to have different JSON Mapper i.e. different JSON mapper configurations, then irrespective of the Spring contexts you'll need a different strategy for the serialization/De-Serialization.
In this case, you will need to implement a Custom De-Serializer {CustomDeSerializer} that will extend JsonDeserializer<T> [com.fasterxml.jackson.databind.JsonDeserializer] and in the deserialize() implement your custom startegy.
Use the #JsonDeserialize(using = CustomDeSerializer.class) annotation on the target POJO.
This way multiple JSON schemes can be managed with different De-Serializers.
By Combining Rest Versioning + Custom Serialization Strategy , each API can be managed in it's own context without having to wire multiple dispatcher Servlet configurations.
Expanding on my comment of yesterday and #Ashoka Header idea i would propose to register 2 MessageConverters (legacy and current) for custom media types. You can do this like that:
#Bean
MappingJackson2HttpMessageConverter currentMappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
// set features
jsonConverter.setObjectMapper(objectMapper);
jsonConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("json", "v2")));
return jsonConverter;
}
#Bean
MappingJackson2HttpMessageConverter legacyMappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
// set features
jsonConverter.setObjectMapper(objectMapper);
return jsonConverter;
}
Pay attention to the custom media-type for one of the converters.
If you like , you can use an Interceptor to rewrite the Version-Headers proposed by #Ashoka to a custom Media-Type like so:
public class ApiVersionMediaTypeMappingInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
try {
if(request.getHeader("X-API-Version") == "2") {
request.setAttribute("Accept:","json/v2");
}
.....
}
}
This might not be the exact answer you were looking for, but maybe it can provide some inspiration. An interceptor is registered like so.
If you can live with a different port for each context, then you only have to overwrite the DispatcherServletAutoConfiguration beans. All the rest of the magic works, multpart, Jackson etc. You can configure the Servlet and Jackson/Multipart etc. for each child-context separately and inject bean of the parent context.
package test;
import static org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME;
import static org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
#Configuration
#EnableAutoConfiguration(exclude = {
Application.Context1.class,
Application.Context2.class
})
public class Application extends WebMvcConfigurerAdapter {
#Bean
public TestBean testBean() {
return new TestBean();
}
public static void main(String[] args) {
final SpringApplicationBuilder builder = new SpringApplicationBuilder().parent(Application.class);
builder.child(Context1.class).run();
builder.child(Context2.class).run();
}
public static class TestBean {
}
#Configuration
#EnableAutoConfiguration(exclude = {Application.class, Context2.class})
#PropertySource("classpath:context1.properties")
public static class Context1 {
#Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
DispatcherServlet dispatcherServlet() {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
// custom config here
return dispatcherServlet;
}
#Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
ServletRegistrationBean dispatcherServletRegistration() {
ServletRegistrationBean registration = new ServletRegistrationBean(dispatcherServlet(), "/test1");
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
// custom config here
return registration;
}
#Bean
Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder(TestBean testBean) {
System.out.println(testBean);
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
// custom config here
return builder;
}
}
#Configuration
#EnableAutoConfiguration(exclude = {Application.class, Context1.class})
#PropertySource("classpath:context2.properties")
public static class Context2 {
#Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
DispatcherServlet dispatcherServlet() {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
// custom config here
return dispatcherServlet;
}
#Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
ServletRegistrationBean dispatcherServletRegistration() {
ServletRegistrationBean registration = new ServletRegistrationBean(dispatcherServlet(), "/test2");
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
// custom config here
return registration;
}
#Bean
Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder(TestBean testBean) {
System.out.println(testBean);
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
// custom config here
return builder;
}
}
}
The context1/2.properties files currently only contain a server.port=8080/8081 but you can set all the other spring properties for the child contexts there.
In Spring-boot ypu can use different profiles (like dev and test).
Start application with
-Dspring.profiles.active=dev
or -Dspring.profiles.active=test
and use different properties files named application-dev.properties or application-test.properties inside your properties directory.
That could do the problem.

Resources