How do I enable strict validation of JSON / Jackso

2020-07-29 00:24发布

问题:

How do I throw an error if extra parameters are specified in the JSON request? For example, "xxx" is not a valid parameter or in the @RequestBody object.

$ curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" -d '{"apiKey": "'$APIKEY'", "email": "name@example.com", "xxx": "yyy"}' localhost:8080/api/v2/stats

I tried adding @Validated to the interface, but it didn't help.

@RequestMapping(value = "/api/v2/stats", method = RequestMethod.POST, produces = "application/json")
public ResponseEntity<DataResponse> stats(Principal principal, @Validated @RequestBody ApiParams apiParams) throws ApiException;

I would like to enable a 'strict' mode so that it will give an error if extra, spurious parameters exist in the request. I could find no way to do this. I found ways to ensure the valid parameters do exist, but no way to ensure there are not extra parameters.


public class ApiParams extends Keyable {

    @ApiModelProperty(notes = "email of user", required = true)
    String email;

public abstract class Keyable {

    @ApiModelProperty(notes = "API key", required = true)
    @NotNull
    String apiKey;

Spring Boot 1.5.20

回答1:

Behind the scene, Spring uses the Jackson library to serialize/deserialize POJO to JSON and vice versa. By default, the ObjectMapper that the framework uses to perform this task has its FAIL_ON_UNKNOWN_PROPERTIES set to false.

You can turn this feature on GLOBALLY by setting the following config value in application.properties.

spring.jackson.deserialization.fail-on-unknown-properties=true

Subsequently, if you want to ignore unknown properties for specific POJO, you can use the annotation @JsonIgnoreProperties(ignoreUnknown=true) in that POJO class.

Still, this is a lot of manual work going forward. Technically, ignoring those unexpected data doesn't violate any software development principles. There might be scenarios where there's a filter or servlet sitting in front of your @Controller doing additional stuff that you're not aware of which requires those extra data. Does it worth the effort?



回答2:

You could try providing a custom implementation for 'MappingJackson2HttpMessageConverter ' class for this message conversion.

    public class CustomMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {

        private static final Logger logger =

        private ObjectMapper objectMapper;

        private boolean prefixJson = false;

        /*
         * (non-Javadoc)
         * 
         * @see org.springframework.http.converter.json.MappingJackson2HttpMessageConverter#setPrefixJson(boolean)
         */
        @Override
        public void setPrefixJson(boolean prefixJson) {
            this.prefixJson = prefixJson;
            super.setPrefixJson(prefixJson);
        }


        /*
         * (non-Javadoc)
         * 
         * @see org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#read(java.lang.reflect.Type,
         * java.lang.Class, org.springframework.http.HttpInputMessage)
         */
        @Override
        public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
                        throws IOException, HttpMessageNotReadableException {
            objectMapper = new ObjectMapper();

/* HERE THIS IS THE PROPERTY YOU ARE INTERESTED IN */
            objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);


            objectMapper.configure(DeserializationFeature.ACCEPT_FLOAT_AS_INT, false);
            objectMapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, true);
            objectMapper.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, true);
            objectMapper.configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, true);

            InputStream istream = inputMessage.getBody();
            String responseString = IOUtils.toString(istream);
            try {
                return objectMapper.readValue(responseString, OperatorTokenDefinition.class);
            } catch (UnrecognizedPropertyException ex) {
               throw new YourCustomExceptionClass();
            } catch (InvalidFormatException ex) { 
               throw new YourCustomExceptionClass();
            } catch (IgnoredPropertyException ex) {
                throw new YourCustomExceptionClass();
            } catch (JsonMappingException ex) {
                throw new YourCustomExceptionClass();
            } catch (JsonParseException ex) {
                logger.error("Could not read JSON JsonParseException:{}", ex);
                throw new YourCustomExceptionClass();
            }
        }

        /*
         * (non-Javadoc)
         * 
         * @see org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#supports(java.lang.Class)
         */
        @Override
        protected boolean supports(Class<?> arg0) {
            return true;
        }

        /*
         * (non-Javadoc)
         * 
         * @see org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#writeInternal(java.lang.Object,
         * org.springframework.http.HttpOutputMessage)
         */
        @Override
        protected void writeInternal(Object arg0, HttpOutputMessage outputMessage)
                        throws IOException, HttpMessageNotWritableException {
            objectMapper = new ObjectMapper();
            String json = this.objectMapper.writeValueAsString(arg0);
            outputMessage.getBody().write(json.getBytes(Charset.defaultCharset()));
        }

        /**
         * @return
         */
        private ResourceBundleMessageSource messageSource() {
            ResourceBundleMessageSource source = new ResourceBundleMessageSource();
            source.setBasename("messages");
            source.setUseCodeAsDefaultMessage(true);
            return source;
        }
    }

Now only we need to register the Custom MessageConverter with the spring context.In the configuration class.Below is the code

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    CustomMappingJackson2HttpMessageConverter jsonConverter =
                    CustomMappingJackson2HttpMessageConverter();
    List<MediaType> mediaTypeList = new ArrayList<MediaType>();
    mediaTypeList.add(MediaType.APPLICATION_JSON);
    jsonConverter.setSupportedMediaTypes(mediaTypeList);
    converters.add(jsonConverter);
    super.configureMessageConverters(converters);
}

Hope that helps ..



回答3:

I know this is not the best solution but still, I'm posting it.

You can implement Interceptor for your controller URL. In preHandle method of your interceptor, you will be able to get HttpServletRequest object from which you can get all the request parameters. In this method, you can write code to write strict validation for request parameters and throw an exception for invalid parameters present in your request.

You can write the validation code in your controller class as well by getting HttpRequest object in your Controller method but it would be good to keep your controller logic and validation logic in separate space.

Interceptor :

public class MyInterceptor extends HandlerInterceptorAdapter {

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception)
    throws Exception {
    // TODO Auto-generated method stub

    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
    throws Exception {
    // TODO Auto-generated method stub

    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        HandlerMethod handlerMethod = (HandlerMethod) handler;

        Map<String, String[]> parameters = request.getParameterMap();
        //Write your validation code

        return true;
    }

}

You should also look at the answers given in How to check for unbound request parameters in a Spring MVC controller method? as well.



回答4:

I found a solution here: https://stackoverflow.com/a/47984837/148844

I added this to my Application.

@SpringBootApplication
public class Application extends SpringBootServletInitializer {

    @Bean
    @Primary
    public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
        return objectMapper;
    }

The @JsonIgnoreProperties was not needed. Now it returns an error like

{"status":"BAD_REQUEST","timestamp":"2019-05-09T05:30:02Z","errorCode":20,"errorMessage":"Malformed JSON request","debugMessage":"JSON parse error: Unrecognized field \"xxx\" (class com.example.ApiParams), not marked as ignorable; nested exception is com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field \"xxx\" (class com.example.ApiParams), not marked as ignorable (2 known properties: \"email\", \"apiKey\"])\n at [Source: java.io.PushbackInputStream@6bec9691; line: 1, column: 113] (through reference chain: com.example.ApiParams[\"xxx\"])"}

(I happen to have a @ControllerAdvice class ResponseEntityExceptionHandler.)