在 Spring Boot 中通过 RequestBodyAdvice 统一解码请求体
在一些数据比较敏感或者对安全要求比较高的应用中,客户端提交给服务器的数据需要进行加密,服务器需要解密后才能获取到原始的请求数据。
在 Spring Boot 中,可以通过 RequestBodyAdvice
对请求体进行统一的解密处理,这对 Controller 来说是完全透明的,极大地提高了应用的可维护性。
RequestBodyAdvice 接口
这是由 spring mvc 提供的一个增强接口,用于在 HttpMessageConverter
读取请求体并把它转换为 Java 对象之前进行一些操作。
public interface RequestBodyAdvice {
boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType);
HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException;
Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
@Nullable
Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
}
supports
:是否要执行此接口,如果返回false
,则该RequestBodyAdvice
会跳过。在这个方法中,可以获取到 Controller 方法中参数及其类型的信息,以及要使用的HttpMessageConverter
信息。beforeBodyRead
:在请求体被读取前执行,在这个方法中,可以获取到完整的请求体,请求头以进行修改。最后,需要返回修改后的HttpInputMessage
。afterBodyRead
:在请求体读取后执行。handleEmptyBody
:如果读取到的请求体是空,则执行。
接下来,我们通过一个示例来演示 RequestBodyAdvice
接口的用法。
假设,客户端 POST 给服务器的所有请求数据都是通过 Base64
进行编码的,我们需要通过 RequestBodyAdvice
进行统一的解码。
定义 @DecodeBody 注解
package cn.springdoc.demo.annotations;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
/**
*
* 需要对请求体进行解码
*
*/
@Retention(RUNTIME)
@Target(PARAMETER)
public @interface DecodeBody {
}
该注解用于 Controller 参数,只有注解了 @DecodeBody
的请求体(@RequestBody
)才需要解码。
Controller
package cn.springdoc.demo.web.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import cn.springdoc.demo.annotations.DecodeBody;
@RestController
@RequestMapping
public class DemoController {
@PostMapping("/demo")
public ResponseEntity<String> demo (@RequestBody @DecodeBody String payload) {
return ResponseEntity.ok(payload);
}
}
一个很简单的 Controller,接收客户端 POST 的字符串,并且原样返回。
使用 @DecodeBody
注解了参数,表示需要对客户端的请求体进行解码。
RequestBodyDecodeAdvice
核心的 RequestBodyAdvice
实现如下。
package cn.springdoc.demo.web.advice;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;
import cn.springdoc.demo.annotations.DecodeBody;
@RestControllerAdvice
public class RequestBodyDecodeAdvice extends RequestBodyAdviceAdapter{
static final Logger log = LoggerFactory.getLogger(RequestBodyDecodeAdvice.class);
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasParameterAnnotation(DecodeBody.class);
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
// 读取完整的客户端请求体,也就是加密/编码后的数据
byte[] payload = StreamUtils.copyToByteArray(inputMessage.getBody());
log.info("加密 Payload:{}", new String(payload));
// 解码为原始数据
byte[] rawPayload = Base64.getDecoder().decode(payload);
log.info("原始 Payload:{}", new String(rawPayload));
// 返回 HttpInputMessage 匿名对象
return new HttpInputMessage() {
@Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
@Override
public InputStream getBody() throws IOException {
// 使用原始数据构建为 ByteArrayInputStream
return new ByteArrayInputStream(rawPayload);
}
};
}
}
这里我们直接继承了 RequestBodyAdviceAdapter
适配器类,它实现了 RequestBodyAdvice
,并且提供了默认实现,所以我们只需要覆写自己感兴趣的方法即可。
通过 @RestControllerAdvice
注解,自动注册到 RequestMappingHandlerAdapter
中。
@RestControllerAdvice
还可以通过其basePackages
和basePackageClasses
属性更细粒度地控制要拦截的 Controller,具体可以参阅 官方文档。
首先,在 supports
方法中判断 Controller 的注解是否有 @DecodeBody
注解,如果没有的话,表示不需要进行解码,返回 false
。
然后,beforeBodyRead
被调用,使用 StreamUtils
工具类读取完整的请求体,也就是客户端 Base64 编码后的数据。接着对其进行解码,得到原文。
最后,返回 HttpInputMessage
对象,请求头不做任何修改,但是请求体使用的是 解码后的原文。
测试
启动服务器,在控制台使用 cURL 发起请求:
$ curl -H "Content-Type: text/plain; charset=UTF-8" -X POST -d "SGVsbG8gc3ByaW5nZG9jLmNu" "http://localhost:8080/demo"
Hello springdoc.cn
如上,POST 到服务器的字符串 SGVsbG8gc3ByaW5nZG9jLmNu
是 Hello springdoc.cn
字符串的 Base64 编码。得到的响应和原文相匹配,说明在 Controller 中得到的请求体,已经是正确解码后的数据了。
后端输出的日志如下:
[nio-8080-exec-8] c.s.d.w.advice.RequestBodyDecodeAdvice : 加密 Payload:SGVsbG8gc3ByaW5nZG9jLmNu
[nio-8080-exec-8] c.s.d.w.advice.RequestBodyDecodeAdvice : 原始 Payload:Hello springdoc.cn
总结
通过实现 RequestBodyAdvice
接口,并注册为 Spring 的 Bean,可以在请求到达 Controller 之前或之后对请求体进行定制化的处理。这样可以实现一些常见的需求,例如请求体解密、数据验签、日志记录等。