Spring MVC接口数据加密传输

前言

假设现在有个需求,要实现接口请求体参数和响应数据的加密传输,换作是你会如何实现呢?
最容易想到的方案就是,在接口方法里直接接收加密后的密文字符串,手动解密成明文,再转换成对应的参数类型,伪代码如下所示:

@PostMapping("api")
public Object api(@RequestBody String encryptedData) {
   
    String decryptedData = decrypt(encryptedData);
    Params params = JSON.parseObject(decryptedData);
    .....
}

这个方案的缺点是代码侵入性太强,接口方法更应该专注于业务。另外就是处理起来太麻烦,会产生很多冗余代码。
有没有更优雅的处理方式呢?

RequestResponseBodyAdviceChain

通过阅读 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

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 的前提下轻松实现数据的加密传输等需求。

相关推荐

  1. SpringMVC接收数据

    2024-01-05 18:18:01       11 阅读
  2. SpringMVC数据传递数据处理

    2024-01-05 18:18:01       36 阅读
  3. 前后端接口写法(传输数据

    2024-01-05 18:18:01       12 阅读
  4. SSH数据加密传输:安全连接新体验

    2024-01-05 18:18:01       16 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-01-05 18:18:01       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-01-05 18:18:01       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-01-05 18:18:01       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-01-05 18:18:01       20 阅读

热门阅读

  1. GreatSQL社区2023全年技术文章总结

    2024-01-05 18:18:01       45 阅读
  2. 7-24 约分最简分式 分数 15

    2024-01-05 18:18:01       33 阅读
  3. centos6后台启动docker

    2024-01-05 18:18:01       32 阅读
  4. 2401vim,vim重要修改更新大全

    2024-01-05 18:18:01       25 阅读
  5. 用RC2CryptoServiceProvider来加密解密

    2024-01-05 18:18:01       41 阅读
  6. 常用材料五行数据库超全汇总

    2024-01-05 18:18:01       30 阅读
  7. 【LeetCode】1321. 餐馆营业额变化增长

    2024-01-05 18:18:01       33 阅读
  8. http请求转发、springboot请求转发、servlet转发请求

    2024-01-05 18:18:01       39 阅读