Spring MVC接口数据加密传输

前言

假设现在有个需求,要实现接口请求体参数和响应数据的加密传输,换作是你会如何实现呢?

最容易想到的方案就是,在接口方法里直接接收加密后的密文字符串,手动解密成明文,再转换成对应的参数类型,伪代码如下所示:

java 复制代码
@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 把请求体转换为方法参数前后做一些拦截处理。

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);
	
	// 请求体为空时的处理
	Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
						   Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
}

实现 ResponseBodyAdvice 接口,允许开发者在响应数据前修改响应体内容。

java 复制代码
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,方便对多个增强器做链式调用。

java 复制代码
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 的增强方法,允许你自定义请求体。

java 复制代码
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,允许你自定义响应内容。

java 复制代码
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;
	}
}

综上所述,这俩扩展点刚好可以满足接口数据加密传输的需求。

实战

如下所示,我们有一个示例接口,用于获取用户信息:

java 复制代码
@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;
    }
}

明文传输时,请求体和响应体是这样的:

json 复制代码
{
    "userId":"1001"
}

{
    "data": {
        "userId": "1001",
        "name": "Lisa",
        "age": 18
    },
    "code": 200,
    "message": "success"
}

现在,我们在不修改接口的情况下,利用上述两个扩展点来实现数据加密传输。

数据加密方式选择对称加密 AES,新建 EncryptRequestResponseBodyAdvice 类,同时实现 RequestBodyAdvice、ResponseBodyAdvice 接口,完成数据加解密逻辑。

java 复制代码
@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;
    }
}

现在请求体必须传输密文了,否则解密会失败。请求体和响应体是这样的:

json 复制代码
{
    "data": "ZejOfDFiQtpMR0jHD14GPVCbr8pRI15j6tmyUjmrcFg="
}

{
    "data": "1DF3cx13KWUOt4bDTvRMDjV4SKCg1P6D8VuZ9yoJIazPxKk66wnk+U1VN9Fqbb2OYyCv4b1s4P5PB4KVDC8IVA==",
    "code": 200,
    "message": "success"
}

响应数据的 data 解密后就是正常的 UserResult 数据。

尾巴

Spring MVC 提供了两个扩展点:RequestBodyAdvice、ResponseBodyAdvice。前者可以在请求体转换为接口方法参数时自定义请求体数据,后者可以在响应@ResponseBody 数据前自定义响应体数据。RequestResponseBodyAdviceChain 同时维护了这两组扩展点实现,以实现统一的链式调用。RequestResponseBodyMethodProcessor 既负责 @RequestBody 参数的解析,又负责 @ResponseBody 数据的响应,解析参数时会自动触发 RequestBodyAdvice 扩展点,响应数据前会触发 ResponseBodyAdvice 扩展点。通过这俩扩展点,我们可以在不修改 Controller 的前提下轻松实现数据的加密传输等需求。

相关推荐
NE_STOP11 分钟前
SpringBoot--简单入门
java·spring
hqxstudying38 分钟前
Java创建型模式---原型模式
java·开发语言·设计模式·代码规范
Dcs1 小时前
VSCode等多款主流 IDE 爆出安全漏洞!插件“伪装认证”可执行恶意命令!
java
保持学习ing1 小时前
day1--项目搭建and内容管理模块
java·数据库·后端·docker·虚拟机
京东云开发者1 小时前
Java的SPI机制详解
java
超级小忍2 小时前
服务端向客户端主动推送数据的几种方法(Spring Boot 环境)
java·spring boot·后端
程序无bug2 小时前
Spring IoC注解式开发无敌详细(细节丰富)
java·后端
小莫分享2 小时前
Java Lombok 入门
java
程序无bug2 小时前
Spring 对于事务上的应用的详细说明
java·后端
食亨技术团队2 小时前
被忽略的 SAAS 生命线:操作日志有多重要
java·后端