数据是企业的第四张名片,企业级开发中少不了数据的加密传输,所以本文介绍下SpringBoot中接口数据加密、解密的方式。
一、加密方案介绍
对接口的加密解密操作主要有下面两种方式:
自定义消息转换器
优势:仅需实现接口,配置简单。
劣势:仅能对同一类型的MediaType进行加解密操作,不灵活。
使用spring提供的接口RequestBodyAdvice和ResponseBodyAdvice
优势:可以按照请求的Referrer、Header或url进行判断,按照特定需要进行加密解密。
比如在一个项目升级的时候,新开发功能的接口需要加解密,老功能模块走之前的逻辑不加密,这时候就只能选择上面的第二种方式了,下面主要介绍下第二种方式加密、解密的过程。
二、实现原理
RequestBodyAdvice可以理解为在@RequestBody之前需要进行的 操作,ResponseBodyAdvice可以理解为在@ResponseBody之后进行的操作,所以当接口需要加解密时,在使用@RequestBody接收前台参数之前可以先在RequestBodyAdvice的实现类中进行参数的解密,当操作结束需要返回数据时,可以在@ResponseBody之后进入ResponseBodyAdvice的实现类中进行参数的加密。
RequestBodyAdvice处理请求的过程:
RequestBodyAdvice源码如下:
- 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);
-
-
- }
调用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 (logger.isDebugEnabled()) {
- logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
- }
- 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;
- }
- }
- }
- catch (IOException ex) {
- throw new HttpMessageNotReadableException("I/O error while reading input message", ex);
- }
-
- if (body == NO_VALUE) {
- if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
- (noContentType && !message.hasBody())) {
- return null;
- }
- throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
- }
-
- return body;
- }
从上面源码可以到当converter.canRead()和message.hasBody()都为true的时候,会调用beforeBodyRead()和afterBodyRead()方法,所以我们在实现类的afterBodyRead()中添加解密代码即可。
ResponseBodyAdvice处理响应的过程:
ResponseBodyAdvice源码如下:
- public interface ResponseBodyAdvice<T> {
-
-
- boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);
-
-
- @Nullable
- T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
- Class<? extends HttpMessageConverter<?>> selectedConverterType,
- ServerHttpRequest request, ServerHttpResponse response);
-
- }
调用ResponseBodyAdvice实现类的部分代码如下:
- if (selectedMediaType != null) {
- selectedMediaType = selectedMediaType.removeQualityValue();
- for (HttpMessageConverter<?> converter : this.messageConverters) {
- GenericHttpMessageConverter genericConverter =
- (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
- if (genericConverter != null ?
- ((GenericHttpMessageConverter) converter).canWrite(declaredType, valueType, selectedMediaType) :
- converter.canWrite(valueType, selectedMediaType)) {
- outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
- (Class<? extends HttpMessageConverter<?>>) converter.getClass(),
- inputMessage, outputMessage);
- if (outputValue != null) {
- addContentDispositionHeader(inputMessage, outputMessage);
- if (genericConverter != null) {
- genericConverter.write(outputValue, declaredType, selectedMediaType, outputMessage);
- }
- else {
- ((HttpMessageConverter) converter).write(outputValue, selectedMediaType, outputMessage);
- }
- if (logger.isDebugEnabled()) {
- logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
- "\" using [" + converter + "]");
- }
- }
- return;
- }
- }
- }
从上面源码可以到当converter.canWrite()为true的时候,会调用beforeBodyWrite()方法,所以我们在实现类的beforeBodyWrite()中添加解密代码即可。
三、实战
新建一个spring boot项目spring-boot-encry,按照下面步骤操作。
pom.xml中引入jar
- <dependencies>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
-
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <optional>true</optional>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>test</scope>
- <exclusions>
- <exclusion>
- <groupId>org.junit.vintage</groupId>
- <artifactId>junit-vintage-engine</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
-
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>fastjson</artifactId>
- <version>1.2.60</version>
- </dependency>
- </dependencies>
请求参数解密拦截类
DecryptRequestBodyAdvice代码如下:
- /**
- * 请求参数 解密操作
- *
- * @Author: Java碎碎念
- * @Date: 2019/10/24 21:31
- *
- */
- @Component
- @ControllerAdvice(basePackages = "com.example.springbootencry.controller")
- @Slf4j
- public class DecryptRequestBodyAdvice implements RequestBodyAdvice {
-
-
- @Override
- public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
- return true;
- }
-
- @Override
- public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> selectedConverterType) throws IOException {
- return inputMessage;
- }
-
- @Override
- public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
- String dealData = null;
- try {
- //解密操作
- Map<String,String> dataMap = (Map)body;
- String srcData = dataMap.get("data");
- dealData = DesUtil.decrypt(srcData);
- } catch (Exception e) {
- log.error("异常!", e);
- }
- return dealData;
- }
-
-
- @Override
- public Object handleEmptyBody(@Nullable Object var1, HttpInputMessage var2, MethodParameter var3, Type var4, Class<? extends HttpMessageConverter<?>> var5) {
- log.info("3333");
- return var1;
- }
-
-
- }
响应参数加密拦截类
EncryResponseBodyAdvice代码如下:
- /**
- * 请求参数 解密操作
- *
- * @Author: Java碎碎念
- * @Date: 2019/10/24 21:31
- *
- */
- @Component
- @ControllerAdvice(basePackages = "com.example.springbootencry.controller")
- @Slf4j
- public class EncryResponseBodyAdvice implements ResponseBodyAdvice<Object> {
-
-
- @Override
- public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
- return true;
- }
-
- @Override
- public Object beforeBodyWrite(Object obj, MethodParameter returnType, MediaType selectedContentType,
- Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest serverHttpRequest,
- ServerHttpResponse serverHttpResponse) {
- //通过 ServerHttpRequest的实现类ServletServerHttpRequest 获得HttpServletRequest
- ServletServerHttpRequest sshr = (ServletServerHttpRequest) serverHttpRequest;
- //此处获取到request 是为了取到在拦截器里面设置的一个对象 是我项目需要,可以忽略
- HttpServletRequest request = sshr.getServletRequest();
-
- String returnStr = "";
-
- try {
- //添加encry header,告诉前端数据已加密
- serverHttpResponse.getHeaders().add("encry", "true");
- String srcData = JSON.toJSONString(obj);
- //加密
- returnStr = DesUtil.encrypt(srcData);
- log.info("接口={},原始数据={},加密后数据={}", request.getRequestURI(), srcData, returnStr);
-
- } catch (Exception e) {
- log.error("异常!", e);
- }
- return returnStr;
- }
新建controller类
TestController代码如下:
- /**
- * @Author: Java碎碎念
- * @Date: 2019/10/24 21:40
- */
- @RestController
- public class TestController {
-
- Logger log = LoggerFactory.getLogger(getClass());
-
- /**
- * 响应数据 加密
- */
- @RequestMapping(value = "/sendResponseEncryData")
- public Result sendResponseEncryData() {
- Result result = Result.createResult().setSuccess(true);
- result.setDataValue("name", "Java碎碎念");
- result.setDataValue("encry", true);
- return result;
- }
-
- /**
- * 获取 解密后的 请求参数
- */
- @RequestMapping(value = "/getRequestData")
- public Result getRequestData(@RequestBody Object object) {
- log.info("controller接收的参数object={}", object.toString());
- Result result = Result.createResult().setSuccess(true);
- return result;
- }
- }
其他类在源码中,后面有github地址
四、测试
访问响应数据加密接口
使用postman发请求http://localhost:8888/sendResponseEncryData,可以看到返回数据已加密,请求截图如下:

响应数据加密截图
后台也打印相关的日志,内容如下:
- 接口=/sendResponseEncryData
-
- 原始数据={"data":{"encry":true,"name":"Java碎碎念"},"success":true}
-
- 加密后数据=vJc26g3SQRU9gAJdG7rhnAx6Ky/IhgioAgdwi6aLMMtyynAB4nEbMxvDsKEPNIa5bQaT7ZAImAL7
- 3VeicCuSTA==
访问请求数据解密接口
使用postman发请求http://localhost:8888/getRequestData,可以看到请求数据已解密,请求截图如下:

请求数据解密截图
后台也打印相关的日志,内容如下:
- 接收到原始请求数据={"data":"VwLvdE8N6FuSxn/jRrJavATopaBA3M1QEN+9bkuf2jPwC1eSofgahQ=="}
-
- 解密后数据={"name":"Java碎碎念","des":"请求参数"}
五、踩到的坑
测试解密请求参数时候,请求体一定要有数据,否则不会调用实现类触发解密操作。
到此SpringBoot中如何灵活的实现接口数据的加解密功能的功能已经全部实现,有问题欢迎留言沟通哦!
完整源码地址: https://github.com/suisui2019/springboot-study
原文链接:http://www.cnblogs.com/haha12/p/11750533.html