如何Spring项目中接口请求参数名称正确性校验?
一般情况下,接口参数校验只会校验参数值是否正确,例如值不能为空,字符串长度,数值范围等,可以通过javax.validation.constraints包下提供的注解类实现。但是在特殊场景下,尤其是接口对公网提供访问时,为了确保接口安全,我们会加强校验。也就是不只是校验参数值是否符合规范,也会对调用方传入的参数名称进行校验,如果传入的参数不在接口文档约定的范围内,进行拦截,并提示调用方参数名不被服务端接受。
实现本需求需要借助Spring中的两个类RequestBodyAdvice,ResponseBodyAdvice。
一、使用场景
参数过滤,数据加解密,日志打印等和业务逻辑无关的全局统一处理场景
二、作用范围
RequestBodyAdvice仅对使用了@RequestBody注解的方法生效 , 因为它原理上还是AOP , 所以GET方法是不会操作的。
三、实现代码
1、定义一个注解类,指明哪些接口方法需要做参数名称的强校验,方法上不加该注解,Spring做请求报文装配时,会自动把不被识别的参数过滤掉
1 @Target(ElementType.METHOD) 2 @Retention(RetentionPolicy.RUNTIME) 3 @Documented 4 public @interface ParamCheckJson { 5 6 }
2、测试的controller类
1 @RestController 2 @RequestMapping("jackson/test") 3 public class JacksonTestController { 4 5 @PostMapping("complexTest") 6 public RpcResponse<BookRpcBean> complexTest(@RequestBody RpcRequest<ComplexRequest> request) { 7 8 return new RpcResponse("0000", "success"); 9 } 10 11 @PostMapping("complexTestUnknown") 12 @ParamCheckJson 13 public RpcResponse<BookRpcBean> complexTestUnknown(@RequestBody RpcRequest<ComplexRequest> request) { 14 15 try { 16 return new RpcResponse("0000", "success"); 17 } catch (VerifyRequestBodyException e) { 18 return new RpcResponse("9999", e.getMessage()); 19 } catch (Exception e) { 20 return new RpcResponse("9999", "系统异常"); 21 } 22 } 23 24 }
3、请求参数校验切面VerifyRequestBodyAdvice类
注意supports方法,这里很关键,supports返回true开启,采用调用下面的beforeBodyRead,afterBodyRead方法,返回false不开启。
通过methodParameter.getMethod().isAnnotationPresent(ParamCheckJson.class)判断controller里的方法是否加了ParamCheckJson注解,是的话执行强校验逻辑。
第34行代码是最关键的地方,我们利用jackson强校验机制来完成参数名称校验,下面列出了JsonTestUtil的关键代码。
1 @RestControllerAdvice 2 public class VerifyRequestBodyAdvice implements RequestBodyAdvice { 3 @Override 4 public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { 5 return methodParameter.getMethod().isAnnotationPresent(ParamCheckJson.class); 6 } 7 8 @Override 9 public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException { 10 return new VerifyHttpInputMessage(inputMessage, targetType); 11 } 12 13 @Override 14 public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { 15 return body; 16 } 17 18 @Override 19 public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { 20 return body; 21 } 22 23 class VerifyHttpInputMessage implements HttpInputMessage { 24 private HttpHeaders headers; 25 private InputStream body; 26 27 public VerifyHttpInputMessage(HttpInputMessage inputMessage, Type targetType) throws IOException { 28 inputMessage.getHeaders().setContentType(MediaType.valueOf("application/json;charset=UTF-8")); 29 String data = IOUtils.toString(inputMessage.getBody()); 30 31 JavaType javaType = JsonTestUtil.getJavaType(targetType); 32 33 try { 34 Object object = JsonTestUtil.readValue(data, javaType); 35 } catch (Exception e) { 36 throw new VerifyRequestBodyException(ExceptionCode.SYS_ERROR, "非法参数名称"+e.getMessage()); 37 } 38 39 40 this.headers = inputMessage.getHeaders(); 41 this.body = IOUtils.toInputStream(data, "UTF-8"); 42 } 43 44 @Override 45 public InputStream getBody() throws IOException { 46 return body; 47 } 48 49 @Override 50 public HttpHeaders getHeaders() { 51 return headers; 52 } 53 } 54 }
4、JsonTestUtil关键代码
1 public class JsonTestUtil { 2 private static final ObjectMapper mapper = new ObjectMapper(); 3 4 static { 5 // 忽略大小写 6 mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); 7 // 该特性决定了当遇到未知属性,是否应该抛出一个JsonMappingException异常。 9 mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true); 10 mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); 11 } 12 13 public static JavaType getJavaType(Type type) { 14 return mapper.getTypeFactory().constructType(type); 15 } 16 17 public static <T> T readValue(String json, JavaType valueType) 18 throws IOException, JsonParseException, JsonMappingException { 19 if (null == json || json.isEmpty()) { 20 return null; 21 } 22 return mapper.readValue(json, valueType); 23 } 24 }
5、返回统一异常处理ExceptionResponseBodyAdvice类,为什么要在这里做异常处理,因为在VerifyRequestBodyAdvice中抛出的自定义异常在controller里面是无法捕获的,因为抛出VerifyRequestBodyException后,请求无法到达complexTestUnknown方法里面。
1 @RestControllerAdvice 2 public class ExceptionResponseBodyAdvice implements ResponseBodyAdvice { 3 @Override 4 public boolean supports(MethodParameter returnType, Class converterType) { 5 return false; 6 } 7 8 @Override 9 public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { 10 return null; 11 } 12 13 @ExceptionHandler(value = VerifyRequestBodyException.class) 14 @ResponseBody 15 public RpcResponse handlerGlobeException(HttpServletRequest request, VerifyRequestBodyException exception) { 16 return new RpcResponse<>(exception.getCode().value(), exception.getMessage()); 17 } 18 }
6、测试
最后通过postman发起调用,返回结果如下
1 { 2 "code": "9999", 3 "message": "非法参数名称Unrecognized field \"aaa\",...... not marked as ignorable ......)", 5 }