Custom validation logic for parameter in REST endpoint Spring Boot

33,909

Solution 1

Best way to do this IMO is to create a custom HandlerMethodArgumentResolver that would look something like this using a custom annotation @Segment:

public class SegmentHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().equals(String.class)
            && parameter.getParameterAnnotation(Segment.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        String apiKey = webRequest.getHeader("X-API-Key");
        if (apiKey != null) {
            if (!API_KEY_LIST.contains(apiKey)) {
                throw new InvalidApiKeyException();
            }
            return apiKey;
        } else {
            return WebArgumentResolver.UNRESOLVED;
        }
    }
}

Then your controller signature looks like this:

@RequestMapping(value = "/example/{id}", method = GET)
 public Response getExample(
         @PathVariable("id") String id,
         @RequestParam(value = "myParam", required = true) @Valid @Pattern(regexp = MY_REGEX) String myParamRequest,
         @RequestParam(value = "callback", required = false) String callback,
         @Segment String apiKeyHeader) {

     // Stuff here...
 }

You'll register the handler method argument resolver in you WebMvcConfigurationAdapter:

@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(segmentHandler());
    }

    @Bean
    public SegmentHandlerMethodArgumentResolver segmentHandler() {
        return new SegmentHandlerMethodArgumentResolver();
    }

}

Solution 2

There is aleardy a feature request Spring backlog, checkout JIRA. However, i was able to achieve what you are trying using @Validated annotation on Controller.

@RestController
@RequestMapping("/user")
@Validated
public class UserController {

   @GetMapping("/{loginId}")
   public User getUserBy(@PathVariable @LoginID final String loginId) {
      // return some user
   }
}

Here @LoginID is custom validator. And @Validated is from org.springframework.validation.annotation.Validated which does the trick.

Solution 3

1) Check manually

You can inject the HttpServletRequest and check the headers.

@RestController
public class HomeController {
    public ResponseEntity<String> test(HttpServletRequest request){
        if(request.getHeader("apiKeyHeader") == null){
            return new ResponseEntity<String>(HttpStatus.BAD_REQUEST);
        }
        return new ResponseEntity<String>(HttpStatus.OK);
    }
}

2) Inject the header

@RequestMapping(value = "/test", method = RequestMethod.POST)
public ResponseEntity<String> test(@RequestHeader(value="myheader") String myheader){
    return new ResponseEntity<String>(HttpStatus.OK);
}

That will return:

{
  "timestamp": 1469805110889,
  "status": 400,
  "error": "Bad Request",
  "exception": "org.springframework.web.bind.ServletRequestBindingException",
  "message": "Missing request header 'myheader' for method parameter of type String",
  "path": "/test"
}

if the header is missing.

3) Use filter

You can automate the check with some filter if you want to use it on multiple methods. In your custom filter just get the header(like in method 1) and if the header is missing just respond with 400 or whatever you want. For me that makes sense when you don't use the header value in the controller method and only need to validate that it's present.

@Bean
public FilterRegistrationBean someFilterRegistration() { 
    FilterRegistrationBean registration = new FilterRegistrationBean();
    registration.setFilter(apiHeaderFilter());
    registration.addUrlPatterns("/example/*");
    registration.setName("apiHeaderFilter");
    registration.setOrder(1);
    return registration;
} 

@Bean(name = "ApiHeaderFilter")
public Filter apiHeaderFilter() {
    return new ApiHeaderFilter();
}

Skip the request

If you use headers attribute in @RequestMapping

@RequestMapping(value = "/test", method = RequestMethod.POST,
    headers = {"content-type=application/json"})

this will result in 404 if there ain't no other handler to take the request.

Share:
33,909
ptimson
Author by

ptimson

Updated on July 18, 2022

Comments

  • ptimson
    ptimson almost 2 years

    I currently have this RequestMapping where I use validation through a regular expression:

     @RequestMapping(value = "/example/{id}", method = GET)
     public Response getExample(
             @PathVariable("id") String id,
             @RequestParam(value = "myParam", required = true) @Valid @Pattern(regexp = MY_REGEX) String myParamRequest,
             @RequestParam(value = "callback", required = false) String callback,
             @RequestHeader(value = "X-API-Key", required = true) @Valid @Pattern(regexp = SEGMENTS_REGEX) String apiKeyHeader) {
    
         // Stuff here...
     }
    

    However, the regulare expression is not enough. Instead, I would like to do some custom validation on the header attribute i.e.

    if (!API_KEY_LIST.contains(apiKeyHeader)) {
        throw Exception();
    }
    

    Is this possible?