Serving static content with custom Content-Type in Spring 5 - spring

I'm trying to set up my app to serve JSON Schema files. That part is easy, and done. What I'm struggling with is making it so that they serve with the correct Content-Type - application/schema+json - instead of the default - application/json.
My configuration is really simple:
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/api/schemas/**")
.addResourceLocations("classpath:/schemas/")
}
But I can't see anything obvious to change any headers except for the caching ones.

Try to override configureContentNegotiation method. This is my code in Java. Bellow, I set "text/javascript" MIME type for javascript modules.
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.mediaType("mjs", new MediaType("text","javascript"));
}

Related

Is it possible to add same-site attribute to Spring Security CSRF's .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())

My security configuration has a following line:
...csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())...
Which sends a csrf cookie with every request back to client. This cookie has no same-site attribute set. Is it possible to add the same-site attribute as well? I looked around some methods inside that class and there is nothing about extra attributes to my knowledge.
How can this be done?
Unfortunately, as of version 4.0.1, the servlet-api doesn't allow you to add the Same-Site attribute to a Cookie. Hopefully this will change soon.
But in the meantime, you could provide your own CsrfTokenRepository implementation that instead of adding a Cookie to the HttpServletResponse (and thus being limited by the servlet-api's representation of a cookie), sets the cookie directly in HTTP header:
public class CustomCsrfTokenRepository implements CsrfTokenRepository {
// implement other methods...
#Override
public void saveToken(CsrfToken token, HttpServletRequest request,
HttpServletResponse response) {
// some version of this:
response.setHeader("Set-Cookie", "HttpOnly; SameSite=strict");
}
}
You can take a look at CookieCsrfTokenRepository to fill in the gaps.
Just to chime in and follow NatFars answer, this solution is especially simple in Kotlin where you can delegate to other objects without having to copy code from the original object in hacky ways:
class CookieCsrfTokenRepositoryWrapper(private val repo: CsrfTokenRepository): CsrfTokenRepository by repo {
override fun saveToken(token: CsrfToken?, req: HttpServletRequest?, res: HttpServletResponse?) {
repo.saveToken(token, req, res)
res?.getHeaders("Set-Cookie")?.toList()?.forEach {
if(it.contains("XSRF") && !it.contains("SameSite"))
res.setHeader("Set-Cookie", "$it; SameSite=strict")
}
}
}
val repo = CookieCsrfTokenRepositoryWrapper(CookieCsrfTokenRepository.withHttpOnlyFalse())
About delegation in Kotlin: https://kotlinlang.org/docs/reference/delegation.html

How to set up cache-control on a 304 reply with Spring-security [duplicate]

I am trying to filter some url pattern to caching.
What I have attempted is put some codes into WebSecurityConfigurerAdapter implementation.
#Override
protected void configure(HttpSecurity http) throws Exception {
initSecurityConfigService();
// For cache
http.headers().defaultsDisabled()
.cacheControl()
.and().frameOptions();
securityConfigService.configure(http,this);
}
However this code will effect all of the web application. How can I apply this to certain URL or Content-Type like images.
I have already tried with RegexRequestMatcher, but it does not work for me.
// For cache
http.requestMatcher(new RegexRequestMatcher("/page/", "GET"))
.headers().defaultsDisabled()
.cacheControl()
.and().frameOptions();
I read this article : SpringSecurityResponseHeaders, but there is no sample for this case.
Thanks.
P.S. In short, I want to remove SpringSecurity defaults for certain url and resources.
What about having multiple WebSecurityConfigurerAdapters? One adapter could have cache controls for certain URLs and another one will not have cache control enabled for those URLs.
I solved this with Filter.
Below is part of my implementation of AbstractAnnotationConfigDispatcherServletInitializer. In onStartup method override.
FilterRegistration.Dynamic springSecurityFilterChain = servletContext.addFilter("springSecurityFilterChain", new DelegatingFilterProxy());
if(springSecurityFilterChain != null){
springSecurityFilterChain.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/render/*", "/service/*");
// I removed pattern url "/image/*" :)
}
What I have done is remove /image/* from MappingUrlPatterns.
Thanks for your answers!

Response MIME type for Spring Boot actuator endpoints

I have updated a Spring Boot application from 1.4.x to 1.5.1 and the Spring Actuator endpoints return a different MIME type now:
For example, /health is now application/vnd.spring-boot.actuator.v1+json instead simply application/json.
How can I change this back?
The endpoints return a content type that honours what the client's request says it can accept. You will get an application/json response if the client send an Accept header that asks for it:
Accept: application/json
In response to the comment of https://stackoverflow.com/users/2952093/kap (my reputation is to low to create a comment): when using Firefox to check endpoints that return JSON I use the Add-on JSONView. In the settings there is an option to specify alternate JSON content types, just add application/vnd.spring-boot.actuator.v1+jsonand you'll see the returned JSON in pretty print inside your browser.
As you noticed the content type for actuators have changed in 1.5.x.
If you in put "application/json" in the "Accept:" header you should get the usual content-type.
But if you don't have any way of modifying the clients, this snippet returns health (without details) and original content-type (the 1.4.x way).
#RestController
#RequestMapping(value = "/health", produces = MediaType.APPLICATION_JSON_VALUE)
public class HealthController {
#Inject
HealthEndpoint healthEndpoint;
#RequestMapping(method = RequestMethod.GET)
public ResponseEntity<Health > health() throws IOException {
Health health = healthEndpoint.health();
Health nonSensitiveHealthResult = Health.status(health.getStatus()).build();
if (health.getStatus().equals(Status.UP)) {
return ResponseEntity.status(HttpStatus.OK).body(nonSensitiveHealthResult);
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(nonSensitiveHealthResult);
}
}
}
Configuration (move away existing health)
endpoints.health.path: internal/health
Based on the code in https://github.com/spring-projects/spring-boot/issues/2449 (which also works fine but completely removes the new type) I came up with
#Component
public class ActuatorCustomizer implements EndpointHandlerMappingCustomizer {
static class Fix extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
Object attribute = request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (attribute instanceof LinkedHashSet) {
#SuppressWarnings("unchecked")
LinkedHashSet<MediaType> lhs = (LinkedHashSet<MediaType>) attribute;
if (lhs.remove(ActuatorMediaTypes.APPLICATION_ACTUATOR_V1_JSON)) {
lhs.add(ActuatorMediaTypes.APPLICATION_ACTUATOR_V1_JSON);
}
}
return true;
}
}
#Override
public void customize(EndpointHandlerMapping mapping) {
mapping.setInterceptors(new Object[] {new Fix()});
}
}
which puts the new vendor-mediatype last so that it will use application/json for all actuator endpoints when nothing is specified.
Tested with spring-boot 1.5.3
Since SpringBoot 2.0.x the suggested solution in implementing the EndpointHandlerMappingCustomizer doesn't work any longer.
The good news is, the solution is simpler now.
The Bean EndpointMediaTypes needs to be provided. It is provided by the SpringBoot class WebEndpointAutoConfiguration by default.
Providing your own could look like this:
#Configuration
public class ActuatorEndpointConfig {
private static final List<String> MEDIA_TYPES = Arrays
.asList("application/json", ActuatorMediaType.V2_JSON);
#Bean
public EndpointMediaTypes endpointMediaTypes() {
return new EndpointMediaTypes(MEDIA_TYPES, MEDIA_TYPES);
}
}
To support application/vnd.spring-boot.actuator.v1+json in Firefox's built in JSON viewer, you can install this addon: json-content-type-override. It will convert content types that contain "json" to "application/json".
Update: Firefox 58+ has built-in support for these mime types, and no addon is needed anymore. See https://bugzilla.mozilla.org/show_bug.cgi?id=1388335

How to make a #RestController POST method ignore Content-Type header and only use request body?

I'm using latest Spring Boot (1.2.1) and whatever Spring MVC version comes with it.
I have a controller method with implicit JSON conversions for both incoming and outgoing data:
#RestController
public class LoginController {
#RequestMapping(value = "/login", method = POST, produces = "application/json")
ResponseEntity<LoginResponse> login(#RequestBody LoginRequest loginRequest) {
// ...
}
}
This works fine, but only if request Content-Type is set to application/json. In all other cases, it responds with 415, regardless of the request body:
{
"timestamp": 1423844498998,
"status": 415,
"error": "Unsupported Media Type",
"exception": "org.springframework.web.HttpMediaTypeNotSupportedException",
"message": "Content type 'text/plain;charset=UTF-8' not supported",
"path": "/login/"
}
Thing is, I'd like to make my API more lenient; I want Spring to only use the POST request body and completely ignore Content-Type header. (If request body is not valid JSON or cannot be parsed into LoginRequest instance, Spring already responds with 400 Bad Request which is fine.) Is this possible while continuing to use the implicit JSON conversions (via Jackson)?
I've tried consumes="*", and other variants like consumes = {"text/*", "application/*"} but it has no effect: the API keeps giving 415 if Content-Type is not JSON.
Edit
It looks like this behaviour is caused by MappingJackson2HttpMessageConverter whose documentation says:
By default, this converter supports application/json and
application/*+json. This can be overridden by setting the supportedMediaTypes property.
I'm still missing how exactly do I customise that, for example in a
custom Jackson2ObjectMapperBuilder...
I assume that you are using default MappingJackson2HttpMessageConverter provided by Spring.
If you would like to have the same behavior in all requests, one solution would be to write custom converter which will not look for Content-Type, in a header (instead will parse to JSON alwayse) and then configure Spring to use your custom one. Again this will affect all requests, so might not fit all needs.
public class CustomerJsonHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
private ObjectMapper mapper = new ObjectMapper();
private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
public CustomerJsonHttpMessageConverter() {
super(new MediaType("application", "json", DEFAULT_CHARSET));
}
#Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException,
HttpMessageNotReadableException {
return mapper.readValue(inputMessage.getBody(), clazz);
}
#Override
protected boolean supports(Class<?> clazz) {
return true;
}
#Override
protected void writeInternal(Object value, HttpOutputMessage outputMessage) throws IOException,
HttpMessageNotWritableException {
String json = mapper.writeValueAsString(value);
outputMessage.getBody().write(json.getBytes());
}
}
To have custom media type,
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(
Arrays.asList(
new MediaType("text", "plain"),
new MediaType("text", "html")
));
For anyone else who is curious about this;
It is possible to customize the used MappingJackson2HttpMessageConverter by overridding WebMvcConfigurerAdapter.extendMessageConverters to allow for multiple mime types.
However, it does not work as expected because application/x-www-form-urlencoded is hardcoded in ServletServerHttpRequest.getBody to modify the body to be url encoded (even if the post data is JSON) before passing it to MappingJackson2HttpMessageConverter.
If you really needed this to work then I think the only way is to put a Filter that modifies the request content-type header before handling (not to imply this is a good idea, just if the situation arises where this is necessary).
Update: watch out if you use this
(This was probably a stupid idea anyway.)
This has the side effect that server sets response Content-Type to whatever the first value in the request's Accept header is! (E.g. text/plain instead of the correct application/json.)
After noticing that, I got rid of this customisation and settled went with Spring's default behaviour (respond with 415 error if request does not have correct Content-Type).
Original answer:
MappingJackson2HttpMessageConverter javadocs state that:
By default, this converter supports application/json and application/*+json. This can be overridden by setting the supportedMediaTypes property.
...which pointed me towards a pretty simple solution that seems to work. In main Application class:
#Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter =
new MappingJackson2HttpMessageConverter(new CustomObjectMapper());
converter.setSupportedMediaTypes(Arrays.asList(MediaType.ALL));
return converter;
}
(CustomObjectMapper is related to other Jackson customisations I have; that contructor parameter is optional.)
This does affect all requests, but so far I don't see a problem with that in my app. If this became a problem, I'd probably just switch the #RequestBody parameter into String, and deserialise it manually.

Jetty HttpServletResponse can't be modified

we want to use jPlayer, an HTML5 audio player with firefox in order to play .ogg files. We noticed that there are issues with HTML5 audio and firefox considering the MEME attribute in the HTTP response. In this case, our content-type resembles "audio/ogg;charset=UTF-8". We think that removing the charset encoding might result in firefox interpreting the file correctly.
Hence, I tried the following in my spring implementation:
response.setContentType("audio/ogg");
response.setCharacterEncoding("");
The first line should not set an encoding. Nevertheless, it's already present before in the response object (recognized this while debugging). The weird thing is: the second line doesn't change anything, the character encoding is not modified. This behavior is totally contradictory to the API description.
Reference: it does not work as described in sample #2 of this post: Jetty Response with no Charset
I appreciate any ideas how-to fix the problem.
Cheers,
Chris
"Works for me..." (although you should set the char encoding to null rather than "")
I wrote some sample code (below) and ran it against 7.4.5
I get the right content type being sent through.
I'm not sure what's going wrong for you - perhaps you could post some code.
My best guess is that you're trying to set the content-type after having already sent content. Since the content type is a header, you need to set it before any of the body gets committed.
public class JettyServer
{
public static class OggServlet extends HttpServlet
{
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
{
File file = new File("src/main/ogg/file.ogg");
response.setContentType("audio/ogg");
response.setCharacterEncoding(null);
response.setContentLength((int) file.length());
FileInputStream in = new FileInputStream(file);
int by;
while ((by = in.read()) != -1)
{
response.getOutputStream().write(by);
}
}
}
public static void main(String[] args) throws Exception
{
Server server = new Server(8080);
ServletContextHandler handler = new ServletContextHandler();
handler.addServlet(OggServlet.class, "/audio");
server.setHandler(handler);
server.start();
}
}
I don't know if this will do anything different, but you could try bypassing the convenience method and calling setHeader method directly:
setHeader("Content-Type", "audio/ogg");
If you really are stumbling over some kind of bug in Jetty, it's worth a shot.

Resources