BAEL-7200
Modify Request Body Before Reaching Controller in Spring Boot
This commit is contained in:
parent
336ee922eb
commit
f90651c5f9
|
@ -49,6 +49,10 @@
|
|||
<artifactId>commons-configuration</artifactId>
|
||||
<version>${commons-configuration.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
@ -61,6 +65,14 @@
|
|||
<layout>JAR</layout>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>9</source>
|
||||
<target>9</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package com.baeldung.modifyrequest;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication(scanBasePackages = "com.baeldung.modifyrequest")
|
||||
public class ModifyRequestApp {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ModifyRequestApp.class, args);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package com.baeldung.modifyrequest.aop;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@RestControllerAdvice
|
||||
@Profile("aspectExample")
|
||||
public class EscapeHtmlAspect implements RequestBodyAdvice {
|
||||
private static final Logger logger = LoggerFactory.getLogger(EscapeHtmlAspect.class);
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
//Apply this to all Controllers
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
|
||||
logger.info("beforeBodyRead called");
|
||||
InputStream inputStream = inputMessage.getBody();
|
||||
return new HttpInputMessage() {
|
||||
@Override
|
||||
public InputStream getBody() throws IOException {
|
||||
return new ByteArrayInputStream(escapeHtml(inputStream).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
return inputMessage.getHeaders();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
// Return the modified object after reading the body
|
||||
return body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
//return the original body
|
||||
return body;
|
||||
}
|
||||
|
||||
private String escapeHtml(InputStream inputStream) throws IOException {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
BufferedReader bufferedReader = null;
|
||||
try (inputStream) {
|
||||
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
|
||||
char[] charBuffer = new char[128];
|
||||
int bytesRead = -1;
|
||||
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
|
||||
stringBuilder.append(charBuffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
String input = stringBuilder.toString();
|
||||
// Escape HTML characters
|
||||
return input.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package com.baeldung.modifyrequest.config;
|
||||
|
||||
import com.baeldung.modifyrequest.interceptor.EscapeHtmlRequestInterceptor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
@Profile("interceptorExample")
|
||||
public class WebMvcConfiguration implements WebMvcConfigurer {
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebMvcConfiguration.class);
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
logger.info("addInterceptors() called");
|
||||
registry.addInterceptor(new EscapeHtmlRequestInterceptor())
|
||||
.addPathPatterns("/save");
|
||||
|
||||
WebMvcConfigurer.super.addInterceptors(registry);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.baeldung.modifyrequest.controller;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/")
|
||||
public class UserController {
|
||||
Logger logger = LoggerFactory.getLogger(UserController.class);
|
||||
|
||||
@PostMapping(value = "save")
|
||||
public ResponseEntity<String> saveUser(@RequestBody String user) {
|
||||
logger.info("save user info into database");
|
||||
ResponseEntity<String> responseEntity = new ResponseEntity<>(user, HttpStatus.CREATED);
|
||||
return responseEntity;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package com.baeldung.modifyrequest.filter;
|
||||
|
||||
import com.baeldung.modifyrequest.requestwrapper.EscapeHtmlRequestWrapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
@Order(1)
|
||||
@Profile("filterExample")
|
||||
public class EscapeHtmlFilter implements Filter {
|
||||
Logger logger = LoggerFactory.getLogger(EscapeHtmlFilter.class);
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
|
||||
throws IOException, ServletException {
|
||||
logger.info("Modify the request");
|
||||
|
||||
filterChain.doFilter(new EscapeHtmlRequestWrapper((HttpServletRequest) servletRequest), servletResponse);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.baeldung.modifyrequest.interceptor;
|
||||
|
||||
import com.baeldung.modifyrequest.requestwrapper.EscapeHtmlRequestWrapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
public class EscapeHtmlRequestInterceptor implements HandlerInterceptor {
|
||||
private static final Logger logger = LoggerFactory.getLogger(EscapeHtmlRequestInterceptor.class);
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
EscapeHtmlRequestWrapper escapeHtmlRequestWrapper = new EscapeHtmlRequestWrapper(request);
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package com.baeldung.modifyrequest.requestwrapper;
|
||||
|
||||
import javax.servlet.ReadListener;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import java.io.*;
|
||||
|
||||
public class EscapeHtmlRequestWrapper extends HttpServletRequestWrapper {
|
||||
private String body = null;
|
||||
public EscapeHtmlRequestWrapper(HttpServletRequest request) throws IOException {
|
||||
super(request);
|
||||
this.body = this.escapeHtml(request);
|
||||
}
|
||||
|
||||
private String escapeHtml(HttpServletRequest request) throws IOException {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
BufferedReader bufferedReader = null;
|
||||
try (InputStream inputStream = request.getInputStream()) {
|
||||
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
|
||||
char[] charBuffer = new char[128];
|
||||
int bytesRead = -1;
|
||||
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
|
||||
stringBuilder.append(charBuffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
String input = stringBuilder.toString();
|
||||
// Escape HTML characters
|
||||
return input.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
//.replaceAll("\"", """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() {
|
||||
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
|
||||
ServletInputStream servletInputStream = new ServletInputStream() {
|
||||
|
||||
@Override
|
||||
public int read() {
|
||||
return byteArrayInputStream.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener listener) {
|
||||
|
||||
}
|
||||
};
|
||||
return servletInputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() throws IOException {
|
||||
return new BufferedReader(new InputStreamReader(this.getInputStream()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
@startuml
|
||||
'https://plantuml.com/sequence-diagram
|
||||
skinparam sequenceMessageAlign direction
|
||||
skinparam handwritten true
|
||||
skinparam sequence {
|
||||
ParticipantBackgroundColor beige
|
||||
ParticipantPadding 50
|
||||
}
|
||||
|
||||
autonumber
|
||||
|
||||
Browser -[#63b175]> Filter: HTTP Request
|
||||
activate Browser
|
||||
activate Filter
|
||||
Filter -[#63b175]> Filter: doFilter()
|
||||
Filter -[#63b175]> DispatcherServlet: HTTP Request
|
||||
activate DispatcherServlet
|
||||
|
||||
|
||||
DispatcherServlet -[#63b175]> Controller: HTTP Request
|
||||
activate Controller
|
||||
Controller --[#63b175]> DispatcherServlet: HTTP Response
|
||||
deactivate Controller
|
||||
|
||||
DispatcherServlet --[#63b175]> Filter: HTTP Response
|
||||
deactivate DispatcherServlet
|
||||
|
||||
Filter --[#63b175]> Browser: HTTP Response
|
||||
deactivate Filter
|
||||
deactivate Browser
|
||||
@enduml
|
|
@ -0,0 +1,33 @@
|
|||
@startuml
|
||||
'https://plantuml.com/sequence-diagram
|
||||
skinparam sequenceMessageAlign direction
|
||||
skinparam handwritten true
|
||||
skinparam sequence {
|
||||
ParticipantBackgroundColor beige
|
||||
ParticipantPadding 50
|
||||
}
|
||||
|
||||
autonumber
|
||||
|
||||
Browser -[#63b175]> Filter: Http Request
|
||||
activate Browser
|
||||
activate Filter
|
||||
Filter -[#63b175]> DispatcherServlet: Http Request
|
||||
activate DispatcherServlet
|
||||
|
||||
DispatcherServlet -[#63b175]> Interceptor: Http Request
|
||||
activate Interceptor
|
||||
Interceptor -[#63b175]> Interceptor: preHandle()
|
||||
Interceptor -[#63b175]> Controller: Http Request
|
||||
activate Controller
|
||||
Controller --[#63b175]> Interceptor: Http Response
|
||||
deactivate Controller
|
||||
Interceptor --[#63b175]> DispatcherServlet: Http Response
|
||||
deactivate Interceptor
|
||||
DispatcherServlet --[#63b175]> Filter: Http Response
|
||||
deactivate DispatcherServlet
|
||||
|
||||
Filter --[#63b175]> Browser: Http Response
|
||||
deactivate Filter
|
||||
deactivate Browser
|
||||
@enduml
|
|
@ -0,0 +1,52 @@
|
|||
package com.baeldung.modifyrequest;
|
||||
|
||||
import com.baeldung.modifyrequest.controller.UserController;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@AutoConfigureMockMvc
|
||||
@WebMvcTest(UserController.class)
|
||||
@ActiveProfiles("aspectExample")
|
||||
public class EscapeHtmlAspectIntegrationTest {
|
||||
Logger logger = LoggerFactory.getLogger(EscapeHtmlAspectIntegrationTest.class);
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
@Test
|
||||
void givenAspect_whenEscapeHtmlAspect_thenEscapeHtml() throws Exception {
|
||||
|
||||
Map<String, String> requestBody = Map.of(
|
||||
"name", "James Cameron",
|
||||
"email", "<script>alert()</script>james@gmail.com"
|
||||
);
|
||||
|
||||
Map<String, String> expectedResponseBody = Map.of(
|
||||
"name", "James Cameron",
|
||||
"email", "<script>alert()</script>james@gmail.com"
|
||||
);
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
mockMvc.perform(MockMvcRequestBuilders.post(URI.create("/save"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(requestBody)))
|
||||
.andExpect(MockMvcResultMatchers.status().isCreated())
|
||||
.andExpect(MockMvcResultMatchers.content().json(objectMapper.writeValueAsString(expectedResponseBody)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package com.baeldung.modifyrequest;
|
||||
|
||||
import com.baeldung.modifyrequest.controller.UserController;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@AutoConfigureMockMvc
|
||||
@WebMvcTest(UserController.class)
|
||||
@ActiveProfiles("filterExample")
|
||||
public class EscapeHtmlFilterIntegrationTest {
|
||||
Logger logger = LoggerFactory.getLogger(EscapeHtmlFilterIntegrationTest.class);
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
@Test
|
||||
void givenFilter_whenEscapeHtmlFilter_thenEscapeHtml() throws Exception {
|
||||
Map<String, String> requestBody = Map.of(
|
||||
"name", "James Cameron",
|
||||
"email", "<script>alert()</script>james@gmail.com"
|
||||
);
|
||||
|
||||
Map<String, String> expectedResponseBody = Map.of(
|
||||
"name", "James Cameron",
|
||||
"email", "<script>alert()</script>james@gmail.com"
|
||||
);
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
mockMvc.perform(MockMvcRequestBuilders.post(URI.create("/save"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(requestBody)))
|
||||
.andExpect(MockMvcResultMatchers.status().isCreated())
|
||||
.andExpect(MockMvcResultMatchers.content().json(objectMapper.writeValueAsString(expectedResponseBody)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package com.baeldung.modifyrequest;
|
||||
|
||||
import com.baeldung.modifyrequest.controller.UserController;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@AutoConfigureMockMvc
|
||||
@WebMvcTest(UserController.class)
|
||||
@ActiveProfiles("interceptorExample")
|
||||
public class EscapeHtmlInterceptorIntegrationTest {
|
||||
Logger logger = LoggerFactory.getLogger(EscapeHtmlInterceptorIntegrationTest.class);
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
void givenInterceptor_whenEscapeHtmlInterceptor_thenEscapeHtml() throws Exception {
|
||||
Map<String, String> requestBody = Map.of(
|
||||
"name", "James Cameron",
|
||||
"email", "<script>alert()</script>james@gmail.com"
|
||||
);
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
mockMvc.perform(MockMvcRequestBuilders.post(URI.create("/save"))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(requestBody)))
|
||||
.andExpect(MockMvcResultMatchers.status().is4xxClientError());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue