假设现在有个需求,要实现接口请求体参数和响应数据的加密传输,换作是你会如何实现呢?
最容易想到的方案就是,在接口方法里直接接收加密后的密文字符串,手动解密成明文,再转换成对应的参数类型,伪代码如下所示:
@PostMapping("api")
public Object api(@RequestBody String encryptedData) {
String decryptedData = decrypt(encryptedData);
Params params = JSON.parseObject(decryptedData);
.....
}
这个方案的缺点是代码侵入性太强,接口方法更应该专注于业务。另外就是处理起来太麻烦,会产生很多冗余代码。
有没有更优雅的处理方式呢?
通过阅读 Spring MVC 的源码,我们发现它提供了两个很有用的接口:RequestBodyAdvice、ResponseBodyAdvice。从名字就可以看出来,它们分别是对请求体和响应体的增强接口。
实现 RequestBodyAdvice 接口,允许开发者在 Spring MVC 把请求体转换为方法参数前后做一些拦截处理。
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);
// 请求体为空时的处理
Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
}
实现 ResponseBodyAdvice 接口,允许开发者在响应数据前修改响应体内容。
public interface ResponseBodyAdvice<T> {
// 是否支持返回值类型
boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);
// 响应数据前置拦截
T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response);
}
还有一个类 RequestResponseBodyAdviceChain,同时实现上述两个接口。内部分别聚合了一组 RequestBodyAdvice 和 ResponseBodyAdvice,方便对多个增强器做链式调用。
class RequestResponseBodyAdviceChain implements RequestBodyAdvice, ResponseBodyAdvice<Object> {
private final List<Object> requestBodyAdvice = new ArrayList<>(4);
private final List<Object> responseBodyAdvice = new ArrayList<>(4);
}
这些增强器会在什么时候触发呢?
RequestResponseBodyMethodProcessor 类既是方法参数解析器,又是返回值处理器。也就是说,它既负责解析@RequestBody
参数,也负责响应@ResponseBody
返回值。
它在解析参数时,会调用AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters
方法,通过 HttpMessageConverter 转换器把请求体转换成目标参数类型。在转换前触发 RequestBodyAdvice 的增强方法,允许你自定义请求体。
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
GenericHttpMessageConverter<?> genericConverter =
(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
if (message.hasBody()) {
// 前置拦截
HttpInputMessage msgToUse =
getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
// 读取
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
// 后置拦截
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
else {
// 处理空请求体
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}
处理返回值也是一个道理,在响应数据前会触发 ResponseBodyAdvice,允许你自定义响应内容。
for (HttpMessageConverter<?> converter : this.messageConverters) {
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
(GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
// MappingJackson2HttpMessageConverter
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
if (body != null) {
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn ->
"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
addContentDispositionHeader(inputMessage, outputMessage);
if (genericConverter != null) {
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
}
else {
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Nothing to write: null body");
}
}
return;
}
}
综上所述,这俩扩展点刚好可以满足接口数据加密传输的需求。
如下所示,我们有一个示例接口,用于获取用户信息:
@RestController
public class UserController {
@PostMapping("user/get")
public R<UserResult> get(@RequestBody GetUserParam param) {
param.checkParams();
UserResult result = new UserResult();
result.setUserId(param.userId);
result.setName("Lisa");
result.setAge(18);
return R.ok(result);
}
@Data
public static class GetUserParam implements Params {
private String userId;
@Override
public void checkParams() {
Assert.hasText(userId, "无效用户ID");
}
}
@Data
public static class UserResult {
private String userId;
private String name;
private Integer age;
}
}
明文传输时,请求体和响应体是这样的:
{
"userId":"1001"
}
{
"data": {
"userId": "1001",
"name": "Lisa",
"age": 18
},
"code": 200,
"message": "success"
}
现在,我们在不修改接口的情况下,利用上述两个扩展点来实现数据加密传输。
数据加密方式选择对称加密 AES,新建 EncryptRequestResponseBodyAdvice 类,同时实现 RequestBodyAdvice、ResponseBodyAdvice 接口,完成数据加解密逻辑。
@ControllerAdvice
public class EncryptRequestResponseBodyAdvice extends RequestBodyAdviceAdapter implements ResponseBodyAdvice<R> {
private final Log log = LogFactory.getLog(EncryptRequestResponseBodyAdvice.class);
private final byte[] key = "B6217B035CD94F78".getBytes();
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return Params.class.isAssignableFrom(methodParameter.getParameterType());
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
return new HttpInputMessage() {
@Override
public InputStream getBody() throws IOException {
InputStream inputStream = inputMessage.getBody();
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes);
String data = JSON.parseObject(bytes).getString("data");
log.info("RequestBody 密文:" + data);
data = SecureUtil.aes(key).decryptStr(data);
log.info("RequestBody 明文:" + data);
return new ByteArrayInputStream(data.getBytes());
}
@Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
};
}
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return R.class.equals(returnType.getParameterType());
}
@Override
public R beforeBodyWrite(R body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body.getData() != null) {
String data = JSON.toJSONString(body.getData());
log.info("ResponseBody 明文:" + data);
data = SecureUtil.aes(key).encryptBase64(data);
log.info("ResponseBody 密文:" + data);
body.setData(data);
}
return body;
}
}
现在请求体必须传输密文了,否则解密会失败。请求体和响应体是这样的:
{
"data": "ZejOfDFiQtpMR0jHD14GPVCbr8pRI15j6tmyUjmrcFg="
}
{
"data": "1DF3cx13KWUOt4bDTvRMDjV4SKCg1P6D8VuZ9yoJIazPxKk66wnk+U1VN9Fqbb2OYyCv4b1s4P5PB4KVDC8IVA==",
"code": 200,
"message": "success"
}
响应数据的 data 解密后就是正常的 UserResult 数据。
Spring MVC 提供了两个扩展点:RequestBodyAdvice、ResponseBodyAdvice。前者可以在请求体转换为接口方法参数时自定义请求体数据,后者可以在响应@ResponseBody
数据前自定义响应体数据。RequestResponseBodyAdviceChain 同时维护了这两组扩展点实现,以实现统一的链式调用。RequestResponseBodyMethodProcessor 既负责 @RequestBody 参数的解析,又负责 @ResponseBody 数据的响应,解析参数时会自动触发 RequestBodyAdvice 扩展点,响应数据前会触发 ResponseBodyAdvice 扩展点。通过这俩扩展点,我们可以在不修改 Controller 的前提下轻松实现数据的加密传输等需求。