From client side am passing an AES encrypted String with Content Type text/plain.
The AES encrypted String is Decrypted before reaching the controller through a Filter.
CustomEncryptedFilter
#Component
#Order(0)
public class CustomEncryptedFilter implements Filter {
private static final Logger logger = LogManager.getLogger(CustomEncryptedFilter.class.getName());
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
logger.info("************** Encryption Filter - START ***********************");
String encryptedString = IOUtils.toString(request.getInputStream());
if (encryptedString != null && encryptedString.length() > 0) {
byte[] decryptedString = new AESEncrytion().decrypt(encryptedString).getBytes();
if (request instanceof HttpServletRequest) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
CustomHttpServletRequestWrapper requestWrapper
= new CustomHttpServletRequestWrapper(httpServletRequest,decryptedString);
logger.info("Content Type: {}", requestWrapper.getContentType());
logger.info("Request Body: {}", IOUtils.toString(requestWrapper.getInputStream()));
chain.doFilter(requestWrapper, response);
} else {
chain.doFilter(request, response);
}
} else {
logger.info("Request is Invalid or Empty");
chain.doFilter(request, response);
}
}
}
Here I will getting the current request body which is an AES encrypted String
then am decrypting it to convert into a String.
encrypted String - Ijwmn5sZ5HqoUPb15c5idjxetqmC8Sln6+d2BPaYzxA=
Original String - {"username":"thivanka"}
After getting the decrypted String (Json object) i am appending it to the request body
by extending HttpServletRequestWrapper
public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
private static final Logger logger = LogManager.getLogger(CustomHttpServletRequestWrapper.class.getName());
private ByteArrayInputStream requestBody;
public CustomHttpServletRequestWrapper(HttpServletRequest request, byte[] decryptedString) {
super(request);
try {
requestBody = new ByteArrayInputStream(decryptedString);
} catch (Exception e) {
logger.error(e);
e.printStackTrace();
}
}
#Override
public String getHeader(String headerName) {
String headerValue = super.getHeader(headerName);
if ("Accept".equalsIgnoreCase(headerName)) {
return headerValue.replaceAll(MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_VALUE);
} else if ("Content-Type".equalsIgnoreCase(headerName)) {
return headerValue.replaceAll(MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_VALUE);
}
return headerValue;
}
#SuppressWarnings("unchecked")
#Override
public Enumeration getHeaderNames() {
HttpServletRequest request = (HttpServletRequest) getRequest();
List list = new ArrayList();
Enumeration e = request.getHeaderNames();
while (e.hasMoreElements()) {
String headerName = (String) e.nextElement();
String headerValue = request.getHeader(headerName);
if ("Accept".equalsIgnoreCase(headerName)) {
headerValue.replaceAll(MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_VALUE);
} else if ("Content-Type".equalsIgnoreCase(headerName)) {
headerValue.replaceAll(MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_VALUE);
}
list.add(headerName);
}
return Collections.enumeration(list);
}
#SuppressWarnings("unchecked")
#Override
public Enumeration getHeaders(final String headerName) {
HttpServletRequest request = (HttpServletRequest) getRequest();
List list = new ArrayList();
Enumeration e = request.getHeaders(headerName);
while (e.hasMoreElements()) {
String header = e.nextElement().toString();
if (header.equalsIgnoreCase(MediaType.TEXT_PLAIN_VALUE)) {
header = MediaType.APPLICATION_JSON_VALUE;
}
list.add(header);
}
return Collections.enumeration(list);
}
#Override
public String getContentType() {
String contentTypeValue = super.getContentType();
if (MediaType.TEXT_PLAIN_VALUE.equalsIgnoreCase(contentTypeValue)) {
return MediaType.APPLICATION_JSON_VALUE;
}
return contentTypeValue;
}
#Override
public BufferedReader getReader() throws UnsupportedEncodingException {
return new BufferedReader(new InputStreamReader(requestBody, "UTF-8"));
}
#Override
public ServletInputStream getInputStream() throws IOException {
return new ServletInputStream() {
#Override
public int read() {
return requestBody.read();
}
#Override
public boolean isFinished() {
// TODO Auto-generated method stub
return false;
}
#Override
public boolean isReady() {
// TODO Auto-generated method stub
return false;
}
#Override
public void setReadListener(ReadListener listener) {
// TODO Auto-generated method stub
}
};
}
}
Apart from adding the new request body am also changing the MediaType from text/plain
to application/json in order for my #RequestBody annotation to pick up the media type and
perform object conversion.
Here's my Controller
#CrossOrigin(origins = "*", allowedHeaders = "*")
#RestController
#RequestMapping("/api/mobc")
public class HomeController {
private static final Logger logger = LogManager.getLogger(HomeController.class.getName());
#RequestMapping(value="/hello", method=RequestMethod.POST,consumes="application/json", produces="application/json")
public ResponseEntity<?> Message(#RequestBody LoginForm loginForm,HttpServletRequest request) {
logger.info("In Home Controller");
logger.info("Content Type: {}", request.getContentType());
return ResponseEntity.status(HttpStatus.OK).body(loginForm);
}
}
LoginForm Object (I removed the Getters/Setters for readability)
public class LoginForm {
private String username;
private String password;
}
Unfortunately am getting the error. What am i doing wrong here.
ExceptionHandlerExceptionResolver - Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing
Possible issue
I suppose that IOUtils.toString(InputStream stream) reads all bytes from the InputStream. But InputStream could be read only once.
Your logging statement
logger.info("Request Body: {}", IOUtils.toString(requestWrapper.getInputStream()));
Reads an InputStream, so Spring can't read it a second time. Try replacing IOUtils.toString(requestWrapper.getInputStream()) with new String(encryptedString, Charset.defaultCharset()).
Other implementation proposal
You can implement custom RequestBodyAdvice which will decrypt the message and change headers if needed.
As from Spring's JavaDoc:
Implementations of this contract may be registered directly with the RequestMappingHandlerAdapter or more likely annotated with #ControllerAdvice in which case they are auto-detected.
Here is an example implementation of advice that changes the first byte of a message to { and last byte to }. Your implementation can modify the message decrypting it.
#ControllerAdvice
class CustomRequestBodyAdvice extends RequestBodyAdviceAdapter {
#Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
try (InputStream inputStream = inputMessage.getBody()) {
byte[] bytes = inputStream.readAllBytes();
bytes[0] = 0x7b; // 0x7b = '{'
bytes[bytes.length - 1] = 0x7d; // 0x7d = '}'
return new CustomMessage(new ByteArrayInputStream(bytes), inputMessage.getHeaders());
}
}
}
class CustomMessage implements HttpInputMessage {
private final InputStream body;
private final HttpHeaders httpHeaders;
public CustomMessage(InputStream body, HttpHeaders httpHeaders) {
this.body = body;
this.httpHeaders = httpHeaders;
}
#Override
public InputStream getBody() throws IOException {
return this.body;
}
#Override
public HttpHeaders getHeaders() {
return this.httpHeaders;
}
}
Also, there is supports method that returns whether this RequestBodyAdvice should be called. In this example this method always returns true, but you can create custom annotation and check for its existence.
// custom annotation
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#interface AesEncrypted {}
// class: CustomRequestBodyAdvice
#Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasParameterAnnotation(AesEncrypted.class);
}
// controller
#PostMapping("one")
String getDecrypted(#AesEncrypted #RequestBody Data data) {
return data.value;
}
If anyone is struggling with this then the answer is to move to a ContentCachingRequestWrapper. Other approach would be to use the aspect oriented variation suggested by #geobreze which solves the same question.
I just had to modify my HttpServletRequestWrapper to facilitate the change.
Refs -> https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times
This class caches the request body by consuming the InputStream. If we
read the InputStream in one of the filters, then other subsequent
filters in the filter chain can't read it anymore. Because of this
limitation, this class is not suitable in all situations.
Related
In my spring boot application I've used a RestTemplateInterceptor to log request and response details in debug mode. To mask the sensitive information in request payload and response body, I've created a custom annotation #LogMaskedStringValue and annotated some fields in request DTO and response DTO. I've created a Serializer MaskStringSerializer to mask the annotated fields with the help of object mapper.
I tried to set the request payload type and expected response body type in request headers and I'm retrieving it in interceptor. But it is not the legitimate way to do, cause the header dependency prevents to use this interceptor in other applications, I tried using RestTemplateRequestCustomizer , Unfortunately it didn't work. Is there any way to get the request payload type and response body type in RestTemplateInterceptor ?
```
#Slf4j
public class RestTemplateLoggingInterceptor implements ClientHttpRequestInterceptor {
private final LogDetailsStorage logDetailsStorage;
private final static ObjectMapper objectMapper = new ObjectMapper();
static {
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
public RestTemplateLoggingInterceptor(LogDetailsStorage logDetailsStorage, String message) {
this.logDetailsStorage = logDetailsStorage;
this.message = message;
}
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
logDetailsStorage.setOutboundStartTime(System.currentTimeMillis());
String requestType = request.getHeaders().getFirst("requestType");
String responseType = request.getHeaders().getFirst("responseType");
request.getHeaders().remove("requestType");
request.getHeaders().remove("responseType");
logRequest(request, body, requestType);
ClientHttpResponse response = execution.execute(request, body);
logResponse(response, responseType);
return response;
}
private void logRequest(HttpRequest request, byte[] body, String requestType) {
if (log.isDebugEnabled()) {
logDetailsStorage.setOutboundRequestUrl(request.getURI().toString());
logDetailsStorage.setOutboundRequestMethod(request.getMethodValue());
MDC.put(MdcKey.OUTBOUND_REQUEST_METHOD.getMdcKey(), logDetailsStorage.getOutboundRequestMethod());
MDC.put(MdcKey.OUTBOUND_REQUEST_URL.getMdcKey(), logDetailsStorage.getOutboundRequestUrl());
if (body != null && body.length > 0) {
String requestPayload = new String(body, StandardCharsets.UTF_8);
logDetailsStorage.setOutboundRequestPayload(getMaskedPayload(requestType, requestPayload));
MDC.put(MdcKey.OUTBOUND_REQUEST_PAYLOAD.getMdcKey(), logDetailsStorage.getOutboundRequestPayload());
}
log.debug("Making request for " + logDetailsStorage.getOutboundRequestUrl());
MDC.remove(MdcKey.OUTBOUND_REQUEST_METHOD.getMdcKey());
MDC.remove(MdcKey.OUTBOUND_REQUEST_URL.getMdcKey());
MDC.remove(MdcKey.OUTBOUND_REQUEST_PAYLOAD.getMdcKey());
}
}
private void logResponse(ClientHttpResponse response, String responseType) throws IOException {
if (log.isDebugEnabled()) {
String responsePayload = StreamUtils.copyToString(response.getBody(), Charset.defaultCharset());
logDetailsStorage.setOutboundResponsePayload(getMaskedPayload(responseType, responsePayload));
logDetailsStorage.setOutboundStatusCode(String.valueOf(response.getRawStatusCode()));
logDetailsStorage.setOutboundExecutionTime((System.currentTimeMillis() - logDetailsStorage.getOutboundStartTime()) / 1000d + " seconds");
MDC.put(MdcKey.OUTBOUND_REQUEST_METHOD.getMdcKey(), logDetailsStorage.getOutboundRequestMethod());
MDC.put(MdcKey.OUTBOUND_REQUEST_URL.getMdcKey(), logDetailsStorage.getOutboundRequestUrl());
MDC.put(MdcKey.OUTBOUND_RESPONSE_PAYLOAD.getMdcKey(), logDetailsStorage.getOutboundResponsePayload());
MDC.put(MdcKey.OUTBOUND_STATUS_CODE.getMdcKey(), logDetailsStorage.getOutboundStatusCode());
if (logDetailsStorage.getOutboundRequestPayload() != null) {
MDC.put(MdcKey.OUTBOUND_REQUEST_PAYLOAD.getMdcKey(), logDetailsStorage.getOutboundRequestPayload());
}
MDC.put(MdcKey.OUTBOUND_EXECUTION_TIME.getMdcKey(), logDetailsStorage.getOutboundExecutionTime());
log.debug("Got Response for "+ logDetailsStorage.getOutboundRequestUrl());
MDC.remove(MdcKey.OUTBOUND_REQUEST_METHOD.getMdcKey());
MDC.remove(MdcKey.OUTBOUND_REQUEST_URL.getMdcKey());
MDC.remove(MdcKey.OUTBOUND_REQUEST_PAYLOAD.getMdcKey());
MDC.remove(MdcKey.OUTBOUND_EXECUTION_TIME.getMdcKey());
MDC.remove(MdcKey.OUTBOUND_STATUS_CODE.getMdcKey());
MDC.remove(MdcKey.OUTBOUND_RESPONSE_PAYLOAD.getMdcKey());
}
}
private String getMaskedPayload(String classType, String payload) {
if (!StringUtils.isEmpty(classType)) {
try {
Object obj = objectMapper.readValue(payload, Class.forName(classType));
payload = LogUtil.getObjectAsMaskedJsonString(obj);
} catch (JsonProcessingException e) {
log.error("'Failed to parse the payload : {}", payload, e);
} catch (ClassNotFoundException e) {
log.error("Class not found exception occurred : {}", classType, e);
}
}
else {
log.warn("ClassType is empty during getMaskedPayload : {}", classType);
}
return payload;
}
}
```
```
public class MaskStringSerializer extends StdSerializer<String> implements ContextualSerializer {
private String mask;
public MaskStringSerializer() {
super(String.class);
}
public MaskStringSerializer(String mask) {
super(String.class);
this.mask = mask;
}
#Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
Optional<String> maskValue = Optional.ofNullable(property)
.map(p -> p.getAnnotation(LogMaskStringValue.class))
.map(LogMaskStringValue::value);
return maskValue.map(MaskStringSerializer::new).orElseGet(MaskStringSerializer::new);
}
#Override
public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException {
if (mask != null) {
gen.writeString(mask);
} else {
gen.writeString(Optional.ofNullable(value).orElse("null"));
}
}
}
```
```
#UtilityClass
#Slf4j
public class LogUtil {
private final static ObjectMapper sensitiveMapper = new ObjectMapper();
static {
SimpleModule module = new SimpleModule();
module.addSerializer(new MaskStringSerializer());
sensitiveMapper.registerModule(module);
}
public static String getObjectAsMaskedJsonString(Object object) {
String requestBody;
try {
requestBody = sensitiveMapper.writeValueAsString(object);
} catch (JsonProcessingException jsonProcessingException) {
log.error("Error while parsing object: {}", object, jsonProcessingException);
requestBody = object.toString();
}
return requestBody;
}
}
```
```
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class Card {
#LogMaskStringValue
private String id;
private String type;
private String last4;
private Integer expirationMonth;
private Integer expirationYear;
}
```
```
`
I have a spring java application that has a EncryptDecryptFilter.java which is a "OncePerRequestFilter" within which I am trying to modify the response payload based on certain conditions. All is going well for happy path.
During the un-happy path, when my application code in the RestController throws an exception, that exception is captured by a #ControllerAdvice class and the controller advice class returns a ResponseEntity. Let's say this response is "string1" with a length 105 characters. After this, the call is intercepted by the "OncePerRequestFilter" and when I attempt to modify the response in this filter with a new response, lets say the modified response is "string2" with a length 200 characters, the modified response string2 is truncated to 105 characters and delievered to the client. So, while the response it self is being modified, the setContentLength() on the response has no effect.
Any suggestions on how to fix this?
See response.setContentLength(encryptedResponse.length()); in the below filter
#Component
public class EncryptDecryptFilter extends OncePerRequestFilter {
public static final Gson gson = new GsonBuilder()
.addSerializationExclusionStrategy(new GsonExclusionStrategy())
.setPrettyPrinting()
.create();
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestURI = request.getRequestURI();
String requestMethod = request.getMethod();
HttpServletRequest requestToUse = new ResettableStreamHttpServletRequest(request);
BufferResponseWrapper responseToUse = new BufferResponseWrapper(response); // declared as a member of this filter class below
interceptRequest(requestToUse, request);
filterChain.doFilter(requestToUse, responseToUse);
interceptResponse(response, responseToUse);
}
private void interceptRequest(HttpServletRequest requestToUse, HttpServletRequest request) throws IOException {
byte[] payload = IOUtils.toByteArray(requestToUse.getReader(), requestToUse.getCharacterEncoding());
String urlEncodedEncryptedBody = new String(payload, requestToUse.getCharacterEncoding());
System.out.println(urlEncodedEncryptedBody);
AesDecryptionRequest decryptionRequest = gson.fromJson(urlEncodedEncryptedBody, AesDecryptionRequest.class);
decryptionRequest.initialize();
String plainTextBody = AesUtil.decrypt(decryptionRequest);
requestToUse.setAttribute("requestBody", plainTextBody);
}
private void interceptResponse(HttpServletResponse response, BufferResponseWrapper responseToUse) throws IOException {
String responseStr = new String(responseToUse.getWrapperBytes());
System.out.println(responseStr); // full response string with length 105 from #ControllerAdvice
AesEncryptionResponse encryptionResponse = AesUtil.encrypt(responseStr);
String encryptedResponse = gson.toJson(encryptionResponse); // encrypted form of #ControllerAdvice response length 200n chars
response.setContentLength(encryptedResponse.length()); // this does not seem to have any effect, the response is still truncated to 105 chars
response.getOutputStream().write(encryptedResponse.getBytes(StandardCharsets.UTF_8));
}
private final class BufferResponseWrapper extends HttpServletResponseWrapper
{
MyServletOutputStream stream = new MyServletOutputStream();
public BufferResponseWrapper(HttpServletResponse httpServletResponse)
{
super(httpServletResponse);
}
public ServletOutputStream getOutputStream() throws IOException
{
return stream;
}
public PrintWriter getWriter() throws IOException
{
return new PrintWriter(stream);
}
public byte[] getWrapperBytes()
{
return stream.getBytes();
}
}
private final class MyServletOutputStream extends ServletOutputStream
{
private ByteArrayOutputStream out = new ByteArrayOutputStream();
public void write(int b) throws IOException
{
out.write(b);
}
public byte[] getBytes()
{
return out.toByteArray();
}
#Override
public boolean isReady() {
return false;
}
#Override
public void setWriteListener(WriteListener writeListener) {
}
}
}
public class ResettableStreamHttpServletRequest extends HttpServletRequestWrapper {
// this class allows the input stream to be read more than once
private static final String UTF_8 = "UTF-8";
private byte[] rawData;
private HttpServletRequest request;
private ResettableServletInputStream servletStream;
public ResettableStreamHttpServletRequest(HttpServletRequest request) {
super(request);
this.request = request;
this.servletStream = new ResettableServletInputStream();
}
public void resetInputStream() {
servletStream.stream = new ByteArrayInputStream(rawData);
}
public void resetInputStream(byte[] newRawData) {
servletStream.stream = new ByteArrayInputStream(newRawData);
}
#Override
public ServletInputStream getInputStream() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getReader(), UTF_8);
servletStream.stream = new ByteArrayInputStream(rawData);
}
return servletStream;
}
#Override
public BufferedReader getReader() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getReader(), UTF_8);
servletStream.stream = new ByteArrayInputStream(rawData);
}
return new BufferedReader(new InputStreamReader(servletStream, UTF_8));
}
private class ResettableServletInputStream extends ServletInputStream {
private ByteArrayInputStream stream;
#Override
public int read() throws IOException {
return stream.read();
}
#Override
// Returns true when all the data from the stream has been read else it returns false.
public boolean isFinished() {
return stream.available() == 0;
}
#Override
// Returns true if data can be read without blocking else returns false.
public boolean isReady() {
return true;
}
#Override
public void setReadListener(ReadListener listener) {
throw new RuntimeException("Not implemented");
}
}
}
Controller Advice class
#ControllerAdvice
public class ExceptionAdvice extends ResponseEntityExceptionHandler {
#ExceptionHandler(value = {Exception.class})
protected ResponseEntity<Object> handleException(Exception exception, WebRequest request) {
return handleExceptionInternal(exception, exception.getMessage(), getStandardHeaders(), HttpStatus.INTERNAL_SERVER_ERROR, request);
}
}
Try using .reset() method on response object before you rewrite it.
private void interceptResponse(HttpServletResponse response, BufferResponseWrapper responseToUse) throws IOException {
String responseStr = new String(responseToUse.getWrapperBytes());
System.out.println(responseStr); // full response string with length 105 from #ControllerAdvice
AesEncryptionResponse encryptionResponse = AesUtil.encrypt(responseStr);
String encryptedResponse = gson.toJson(encryptionResponse); // encrypted form of #ControllerAdvice response length 200n chars
response.reset(); <-------------
response.setContentLength(encryptedResponse.length()); // this does not seem to have any effect, the response is still truncated to 105 chars
response.getOutputStream().write(encryptedResponse.getBytes(StandardCharsets.UTF_8));
}
According to the doc
void reset() Clears any data that exists in the buffer as well as the
status code and headers. If the response has been committed, this
method throws an IllegalStateException.
Throws: IllegalStateException
if the response has already been committed
I am writing a Spring boot filter to verify request data before it hits the rest controller. To avoid HttpServletRequest read ServletInputStream twice, I writed a CustomRequestWrapper to wrap it. But When I send a post request(conetentType = "multipart/form-data") by postman , I get a null in rest controller.
Here is my filter:
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
CustomReqeustWrapper requestWrapper = new CustomReqeustWrapper(req);
CustomResponseWrapper responseWrapper = new CustomResponseWrapper(resp);
// get requestBody from requestWrapper and verify.
readContent(requestWrapper);
chain.doFilter(requestWrapper, responseWrapper);
writeContent(response);
}
Here is my RequestWrapper:
public class CustomRequestWrapper extends HttpServletRequestWrapper {
private byte[] requestBody = new byte[0];
public CustomRequestWrapper (HttpServletRequest request) {
super(request);
try {
requestBody = StreamUtils.copyToByteArray(request.getInputStream());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
#Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
#Override
public int read() throws IOException {
return bais.read();
}
#Override
public boolean isFinished() {
return false;
}
#Override
public boolean isReady() {
return true;
}
#Override
public void setReadListener(ReadListener listener) {
}
};
}
public byte[] getRequestData() {
return requestBody;
}
public void setRequestData(byte[] requestData) {
this.requestBody = requestData;
}
}
Here is my controller:
#PostMapping("/exchange")
public ResponseEntity<Void> keyExchange(HttpServletRequest request, String Key) throws Exception {
// get the "key" is null.
}
If the type of parameter 'key' is String,maybe you should't use 'multipart' data type.
For all I know,I had met this problem once.The wrapper is useless for file parameter.
If you have to use 'multipart' type,perhaps you should use other manner to figure out it.
I have Spring Boot and I need to log user action in DB, so I wrote HandlerInterceptor:
#Component
public class LogInterceptor implements HandlerInterceptor {
#Autovired
private LogUserActionService logUserActionService;
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws IOException {
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
String url = request.getRequestURI();
String queryString = request.getQueryString() != null ? request.getQueryString() : "";
String body = "POST".equalsIgnoreCase(request.getMethod()) ? new BufferedReader(new InputStreamReader(request.getInputStream())).lines().collect(Collectors.joining(System.lineSeparator())) : queryString;
logUserActionService.logUserAction(userName, url, body);
return true;
}
}
But according to this answer Get RequestBody and ResponseBody at HandlerInterceptor "RequestBody can be read only once", so as I understand I read input stream and then Spring tries to do same, but stream has been read already and I'm getting an error: "Required request body is missing ..."
So I tried different ways to make buffered input stream i.e.:
HttpServletRequest httpServletRequest = new ContentCachingRequestWrapper(request);
new BufferedReader(new InputStreamReader(httpServletRequest.getInputStream())).lines().collect(Collectors.joining(System.lineSeparator()))
Or
InputStream bufferedInputStream = new BufferedInputStream(request.getInputStream());
But nothing helped
Also I tried to use
#ControllerAdvice
public class UserActionRequestBodyAdviceAdapter extends RequestBodyAdviceAdapter {
But it has only body, no request info like URL or Request parameters
Also tried to use Filters, but result same.
So I need a good way to get information from request like user, URL, parameters, body (if present) and write it to DB.
To log HTTP Request & Response, you can use RequestBodyAdviceAdapter and ResponseBodyAdvice. here, it is using in my way.
CustomRequestBodyAdviceAdapter.java
#ControllerAdvice
public class CustomRequestBodyAdviceAdapter extends RequestBodyAdviceAdapter {
#Autowired
HttpServletRequest httpServletRequest;
#Override
public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
#Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
// here you can full log httpServletRequest and body.
return super.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
}
}
CustomResponseBodyAdviceAdapter.java
#ControllerAdvice
public class CustomResponseBodyAdviceAdapter implements ResponseBodyAdvice<Object> {
#Autowired
private LoggingService loggingService;
#Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
#Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType,
Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if (serverHttpRequest instanceof ServletServerHttpRequest && serverHttpResponse instanceof ServletServerHttpResponse) {
// here you can full log httpServletRequest and body.
}
return o;
}
}
Above AdviceAdapter cannot handle the GET request. So, you can use HandlerInterceptor.
CustomWebConfigurerAdapter.java
#Component
public class CustomWebConfigurerAdapter implements WebMvcConfigurer {
#Autowired
private CustomLogInterceptor httpServiceInterceptor;
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(httpServiceInterceptor);
}
}
CustomLogInterceptor.java
#Component
public class CustomLogInterceptor implements HandlerInterceptor {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (DispatcherType.REQUEST.name().equals(request.getDispatcherType().name()) && request.getMethod().equals(HttpMethod.GET.name())) {
// here you can full log httpServletRequest and body for GET Request.
}
return true;
}
}
Here you can reference full source code in my git.
springboot-http-request-response-loging-with-json-logger
+Feature => It is already have Integration with ELK (Elasticsearch, Logstash, Kibana)
You can use Filter to log request body.
public class LoggingFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
try {
chain.doFilter(wrappedRequest, res);
} finally {
logRequestBody(wrappedRequest);
}
}
private static void logRequestBody(ContentCachingRequestWrapper request) {
byte[] buf = request.getContentAsByteArray();
if (buf.length > 0) {
try {
String requestBody = new String(buf, 0, buf.length, request.getCharacterEncoding());
System.out.println(requestBody);
} catch (Exception e) {
System.out.println("error in reading request body");
}
}
}
}
The main thing to note here is that you have to pass object of ContentCachingRequestWrapper in filter chain otherwise you won't get request content in it.
In above example, if you use chain.doFilter(req, res) or chain.doFilter(request, res) then you won't get request body in wrappedRequest object.
You can get the Request Body data using RequestBodyAdviceAdapter for POST/PUT requests. You can use HandlerInterceptorAdapter for GET calls. Here's a working example -
https://frandorado.github.io/spring/2018/11/15/log-request-response-with-body-spring.html
#ControllerAdvice
public class CustomRequestBodyAdviceAdapter extends RequestBodyAdviceAdapter
{
#Autowired
HttpServletRequest httpServletRequest;
private static final Log LOGGER = LogFactory.getLog(CustomRequestBodyAdviceAdapter.class);
private static final Charset DEFAULT_CHARSET = ISO_8859_1;
#Override
public boolean supports(MethodParameter methodParameter, Type type,
Class<? extends HttpMessageConverter<?>> aClass)
{
return true;
}
#Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage,
MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType)
{
Instant startTime = Instant.now();
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("REQUEST call Starts :: Start Time : %s ").append(startTime);
try
{
logRequest(httpServletRequest, body);
}
catch (IOException e)
{
LOGGER.info("Exception getting the Request Body into the Log: {}" + e.getMessage());
}
public void logRequest(HttpServletRequest httpServletRequest, Object body) throws IOException
{
StringBuilder stringBuilder = new StringBuilder();
Map<String, String> parameters = buildParametersMap(httpServletRequest);
stringBuilder.append("REQUEST ");
stringBuilder.append("method=[").append(httpServletRequest.getMethod()).append("] ");
stringBuilder.append("path=[").append(httpServletRequest.getRequestURI()).append("] ");
stringBuilder.append("headers=[").append(buildHeadersMap(httpServletRequest)).append("] ");
if (!parameters.isEmpty())
{
stringBuilder.append("parameters=[").append(parameters).append("] ");
}
if (body != null)
{
stringBuilder.append("body=[" + body + "]");
}
ObjectMapper objectMapper = new ObjectMapper();
String jsonInString = null;
try
{
jsonInString = objectMapper.writer().writeValueAsString(body);
}
catch (JsonProcessingException e)
{
throw new RestApiException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
}
stringBuilder.append("REQUEST Body = [").append(jsonInString).append("] ");
LOGGER.info("BODY DATA >>>> " + jsonInString);
LOGGER.info("Body - : {}" + stringBuilder);
}
private Map<String, String> buildParametersMap(HttpServletRequest httpServletRequest)
{
Map<String, String> resultMap = new HashMap<>();
Enumeration<String> parameterNames = httpServletRequest.getParameterNames();
while (parameterNames.hasMoreElements())
{
String key = parameterNames.nextElement();
String value = httpServletRequest.getParameter(key);
resultMap.put(key, value);
}
return resultMap;
}
private Map<String, String> buildHeadersMap(HttpServletRequest request)
{
Map<String, String> map = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements())
{
String key = headerNames.nextElement();
String value = request.getHeader(key);
map.put(key, value);
}
return map;
}
}
I have used ObjectMapper here because I need the body response as raw JSON object, but the afterBodyRead() is invoked after the body is transformed to Java Object.
I found this solved my problem for copying the request buffer for application/json content types. It also shows how to extend the wrapper as the comments to Harshit solution mentions.
https://levelup.gitconnected.com/how-to-log-the-request-body-in-a-spring-boot-application-10083b70c66
The important pieces are that you need a filter to pass along the new request to the server.
#Component
public class LoggingFilter implements Filter {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
if (Arrays.asList("POST", "PUT").contains(httpRequest.getMethod())) {
CustomHttpRequestWrapper requestWrapper = new CustomHttpRequestWrapper(httpRequest);
requestWrapper.setAttribute("input", requestWrapper.getBodyInStringFormat());
filterChain.doFilter(requestWrapper, servletResponse);
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
The logger requires a custom wrapper, and the one spring boot provides seems to be insufficient for application/json type messages.
public class CustomHttpRequestWrapper extends HttpServletRequestWrapper {
public String getBodyInStringFormat() {
return bodyInStringFormat;
}
private final String bodyInStringFormat;
public CustomHttpRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
bodyInStringFormat = readInputStreamInStringFormat(request.getInputStream(), Charset.forName(request.getCharacterEncoding()));
}
private String readInputStreamInStringFormat(InputStream stream, Charset charset) throws IOException {
return getString(stream, charset);
}
static String getString(InputStream stream, Charset charset) throws IOException {
final int MAX_BODY_SIZE = 1024;
final StringBuilder bodyStringBuilder = new StringBuilder();
if (!stream.markSupported()) {
stream = new BufferedInputStream(stream);
}
stream.mark(MAX_BODY_SIZE + 1);
final byte[] entity = new byte[MAX_BODY_SIZE + 1];
final int bytesRead = stream.read(entity);
if (bytesRead != -1) {
bodyStringBuilder.append(new String(entity, 0, Math.min(bytesRead, MAX_BODY_SIZE), charset));
if (bytesRead > MAX_BODY_SIZE) {
bodyStringBuilder.append("...");
}
}
stream.reset();
return bodyStringBuilder.toString();
}
#Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
#Override
public ServletInputStream getInputStream () {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bodyInStringFormat.getBytes());
return new ServletInputStream() {
private boolean finished = false;
#Override
public boolean isFinished() {
return finished;
}
#Override
public int available() {
return byteArrayInputStream.available();
}
#Override
public void close() throws IOException {
super.close();
byteArrayInputStream.close();
}
#Override
public boolean isReady() {
return true;
}
#Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
public int read () {
int data = byteArrayInputStream.read();
if (data == -1) {
finished = true;
}
return data;
}
};
}
}
I would like to get the XML data from request and response and use it into Rest controller. I tried this:
#RestController()
public class HomeController {
#PostMapping(value = "/v1")
public Response handleMessage(#RequestBody Transaction transaction, HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest request, HttpServletResponse response
System.out.println("!!!!!!! InputStream");
System.out.println(request.getInputStream());
System.out.println(response.getOutputStream());
InputStream in = request.getInputStream();
String readLine;
BufferedReader br = new BufferedReader(new InputStreamReader(in));
while (((readLine = br.readLine()) != null)) {
System.out.println(readLine);
}
}
}
But I get java.io.IOException: UT010029: Stream is closed
What is the proper way to get the content into String variable?
EDIT: I also tried solution with Filter but I'm not aware how to use the request payload into rest controller:
Read request payload:
#Component
public class HttpLoggingFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(HttpLoggingFilter.class);
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
ResettableStreamHttpServletRequest wrappedRequest = new ResettableStreamHttpServletRequest((HttpServletRequest) request);
wrappedRequest.getInputStream().read();
String body = IOUtils.toString(wrappedRequest.getReader());
System.out.println("!!!!!!!!!!!!!!!!!! " + body);
wrappedRequest.resetInputStream();
chain.doFilter(request, response);
}
public class ResettableStreamHttpServletRequest extends HttpServletRequestWrapper {
private byte[] rawData;
private HttpServletRequest request;
private ResettableServletInputStream servletStream;
ResettableStreamHttpServletRequest(HttpServletRequest request) {
super(request);
this.request = request;
this.servletStream = new ResettableServletInputStream();
}
void resetInputStream() {
servletStream.stream = new ByteArrayInputStream(rawData);
}
#Override
public ServletInputStream getInputStream() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getInputStream());
servletStream.stream = new ByteArrayInputStream(rawData);
}
return servletStream;
}
#Override
public BufferedReader getReader() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getInputStream());
servletStream.stream = new ByteArrayInputStream(rawData);
}
String encoding = getCharacterEncoding();
if (encoding != null) {
return new BufferedReader(new InputStreamReader(servletStream, encoding));
} else {
return new BufferedReader(new InputStreamReader(servletStream));
}
}
private class ResettableServletInputStream extends ServletInputStream {
private InputStream stream;
#Override
public int read() throws IOException {
return stream.read();
}
#Override
public boolean isFinished() {
// TODO Auto-generated method stub
return false;
}
#Override
public boolean isReady() {
// TODO Auto-generated method stub
return false;
}
#Override
public void setReadListener(ReadListener readListener) {
// TODO Auto-generated method stub
}
}
}
}
Rest endpoint:
#RestController()
public class HomeController {
#PostMapping(value = "/v1")
public Response handleMessage(#RequestBody Transaction transaction, HttpServletRequest request, org.zalando.logbook.HttpRequest requestv, HttpServletResponse response) throws Exception {
// Get here request and response and log it into DB
}
}
How I can call HttpLoggingFilter into the Java method handleMessage and get the request as body String? Probably I can make it service and Inject it? Can you give me some advice how I can assess the code?
Here are a bunch of classes to do it. This is a once a OncePerRequestFilter implementation, check here https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/filter/OncePerRequestFilter.html. Basically the problem is that in the chain filter, the request stream and response stream can be read just once. So, need to wrap these 2 streams inside something that can be read more than once.
In the first 2 lines I wrapped request and response inside requestToUse and responseToUse. ResettableStreamHttpServletRequest and ResettableStreamHttpServletResponse are wrapper classes that keeps the original string body inside of them, and every time the stream is needed they return a new stream.Then from there, you forget about request and response and start using requestToUse and responseToUse.
I took this from an old project I did. Actually there are more clases, but I extracted the main parts for you. This may not compile right away. Give it a try and let me know and I will help you to make it work.
public class RequestResponseLoggingFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//here you wrap the request and response into some resetable istream class
HttpServletRequest requestToUse = new ResettableStreamHttpServletRequest(request);
HttpServletResponse responseToUse = new ResettableStreamHttpServletResponse(response);
//you read the request to log it
byte[] payload = IOUtils.toByteArray(requestToUse.getReader(), requestToUse.getCharacterEncoding());
String body = new String(payload, requestToUse.getCharacterEncoding());
//here you log the body request
log.(body);
//let the chain continue
filterChain.doFilter(requestToUse, responseToUse);
// Here we log the response
String response = new String(responseToUse.toString().getBytes(), responseToUse.getCharacterEncoding());
//since you can read the stream just once, you will need it again for chain to be able to continue, so you reset it
ResettableStreamHttpServletResponse responseWrapper = WebUtils.getNativeResponse(responseToUse, ResettableStreamHttpServletResponse.class);
if (responseWrapper != null) {
responseWrapper.copyBodyToResponse(true);
}
}
}
public class ResettableStreamHttpServletRequest extends HttpServletRequestWrapper {
private byte[] rawData;
private ResettableServletInputStream servletStream;
public ResettableStreamHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
rawData = IOUtils.toByteArray(request.getInputStream());
servletStream = new ResettableServletInputStream();
servletStream.setStream(new ByteArrayInputStream(rawData));
}
#Override
public ServletInputStream getInputStream() throws IOException {
servletStream.setStream(new ByteArrayInputStream(rawData));
return servletStream;
}
#Override
public BufferedReader getReader() throws IOException {
servletStream.setStream(new ByteArrayInputStream(rawData));
return new BufferedReader(new InputStreamReader(servletStream));
}
}
public class ResettableStreamHttpServletResponse extends HttpServletResponseWrapper {
private ByteArrayServletOutputStream byteArrayServletOutputStream = new ByteArrayServletOutputStream();
public ResettableStreamHttpServletResponse(HttpServletResponse response) throws IOException {
super(response);
}
/**
* Copy the cached body content to the response.
*
* #param complete whether to set a corresponding content length for the complete cached body content
* #since 4.2
*/
public void copyBodyToResponse(boolean complete) throws IOException {
byte[] array = byteArrayServletOutputStream.toByteArray();
if (array.length > 0) {
HttpServletResponse rawResponse = (HttpServletResponse) getResponse();
if (complete && !rawResponse.isCommitted()) {
rawResponse.setContentLength(array.length);
}
rawResponse.getOutputStream().write(byteArrayServletOutputStream.toByteArray());
if (complete) {
super.flushBuffer();
}
}
}
/**
* The default behavior of this method is to return getOutputStream() on the wrapped response object.
*/
#Override
public ServletOutputStream getOutputStream() throws IOException {
return byteArrayServletOutputStream;
}
/**
* The default behavior of this method is to return getOutputStream() on the wrapped response object.
*/
#Override
public String toString() {
String response = new String(byteArrayServletOutputStream.toByteArray());
return response;
}
}
You dont need to do anything special here, Spring framework will do it for you.
All you need is:
Create a Pojo or Bean which represents your XML data.
Add xml data format dependency to Gradle/Maven which will bind the request xml to your pojo.
compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.9.9'
Tell your request handler to accept XML like this:
#RequestMapping(value = "/xmlexample", method = RequestMethod.POST,consumes = "application/xml;charset=UTF-8")
public final boolean transactionHandler(#Valid #RequestBody Transaction transaction) {
log.debug("Received transaction request with data {}", transaction);
return true;
}
And voila, you will have your transaction bean populated with your XML data.