简体   繁体   English

@RequestBody 无法转换从 AES 加密字符串派生的 object

[英]@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.从客户端传递带有内容类型文本/纯文本的AES加密字符串。

The AES encrypted String is Decrypted before reaching the controller through a Filter. AES 加密字符串在通过过滤器到达 controller 之前被解密。

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.在这里,我将获取当前请求正文,它是一个 AES 加密字符串,然后将其解密以转换为字符串。

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在获得解密的字符串(Json 对象)后,我通过扩展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.除了添加新的请求正文之外,我还将 MediaType 从text/plain更改为application/json ,以便我的 @RequestBody 注释获取媒体类型并执行 object 转换。

Here's my Controller这是我的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) LoginForm Object(为了便于阅读,我删除了 Getters/Setters)

public class LoginForm {公共 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 .我想IOUtils.toString(InputStream stream)InputStream读取所有字节。 But InputStream could be read only once.但是InputStream只能读取一次。

Your logging statement您的日志记录语句

logger.info("Request Body: {}", IOUtils.toString(requestWrapper.getInputStream()));

Reads an InputStream , so Spring can't read it a second time.读取InputStream ,因此 Spring 无法再次读取它。 Try replacing IOUtils.toString(requestWrapper.getInputStream()) with new String(encryptedString, Charset.defaultCharset()) .尝试用new String(encryptedString, Charset.defaultCharset())替换IOUtils.toString(requestWrapper.getInputStream()) )) 。

Other implementation proposal其他实施方案

You can implement custom RequestBodyAdvice which will decrypt the message and change headers if needed.您可以实现自定义RequestBodyAdvice ,它将解密消息并在需要时更改标头。

As from Spring's JavaDoc:从 Spring 的 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.该合约的实现可以直接使用 RequestMappingHandlerAdapter 注册,或者更可能使用 @ControllerAdvice 进行注释,在这种情况下它们会被自动检测到。

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.此外,还有一个RequestBodyAdvice supports In this example this method always returns true , but you can create custom annotation and check for its existence.在此示例中,此方法始终返回true ,但您可以创建自定义注释并检查其是否存在。

// 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.如果有人为此而苦苦挣扎,那么答案是转移到 ContentCachingRequestWrapper。 Other approach would be to use the aspect oriented variation suggested by @geobreze which solves the same question.其他方法是使用@geobreze 建议的面向方面的变体来解决相同的问题。

I just had to modify my HttpServletRequestWrapper to facilitate the change.我只需要修改我的 HttpServletRequestWrapper 以促进更改。

Refs -> https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times参考-> https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times

This class caches the request body by consuming the InputStream.此 class 通过使用 InputStream 缓存请求正文。 If we read the InputStream in one of the filters, then other subsequent filters in the filter chain can't read it anymore.如果我们在其中一个过滤器中读取 InputStream,那么过滤器链中的其他后续过滤器将无法再读取它。 Because of this limitation, this class is not suitable in all situations.由于此限制,此 class 并非适用于所有情况。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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