简体   繁体   中英

@RequestBody not able to convert object derived from AES Encrypted String

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM