SpringBoot中如何灵活的实现接口数据的加解密功能?
數據是企業的第四張名片,企業級開發中少不了數據的加密傳輸,所以本文介紹下SpringBoot中接口數據加密、解密的方式。
本文目錄
一、加密方案介紹二、實現原理三、實戰四、測試五、踩到的坑
一、加密方案介紹
對接口的加密解密操作主要有下面兩種方式:
自定義消息轉換器
優勢:僅需實現接口,配置簡單。
使用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);@NullableObject?handleEmptyBody(@Nullable?Object?body,?HttpInputMessage?inputMessage,?MethodParameter?parameter,Type?targetType,?Class<??extends?HttpMessageConverter<?>>?converterType);}調用RequestBodyAdvice實現類的部分代碼如下:
?protected?<T>?Object?readWithMessageConverters(HttpInputMessage?inputMessage,?MethodParameter?parameter,Type?targetType)?throws?IOException,?HttpMediaTypeNotSupportedException,?HttpMessageNotReadableException?{MediaType?contentType;boolean?noContentType?=?false;try?{contentType?=?inputMessage.getHeaders().getContentType();}catch?(InvalidMediaTypeException?ex)?{throw?new?HttpMediaTypeNotSupportedException(ex.getMessage());}if?(contentType?==?null)?{noContentType?=?true;contentType?=?MediaType.APPLICATION_OCTET_STREAM;}Class<?>?contextClass?=?parameter.getContainingClass();Class<T>?targetClass?=?(targetType?instanceof?Class???(Class<T>)?targetType?:?null);if?(targetClass?==?null)?{ResolvableType?resolvableType?=?ResolvableType.forMethodParameter(parameter);targetClass?=?(Class<T>)?resolvableType.resolve();}HttpMethod?httpMethod?=?(inputMessage?instanceof?HttpRequest???((HttpRequest)?inputMessage).getMethod()?:?null);Object?body?=?NO_VALUE;EmptyBodyCheckingHttpInputMessage?message;try?{message?=?new?EmptyBodyCheckingHttpInputMessage(inputMessage);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);@NullableT?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
請求參數解密攔截類
DecryptRequestBodyAdvice代碼如下:
/***?請求參數?解密操作**?@Author:?Java碎碎念*?@Date:?2019/10/24?21:31**/ @Component @ControllerAdvice(basePackages?=?"com.example.springbootencry.controller") @Slf4j public?class?DecryptRequestBodyAdvice?implements?RequestBodyAdvice?{@Overridepublic?boolean?supports(MethodParameter?methodParameter,?Type?targetType,?Class<??extends?HttpMessageConverter<?>>?converterType)?{return?true;}@Overridepublic?HttpInputMessage?beforeBodyRead(HttpInputMessage?inputMessage,?MethodParameter?methodParameter,?Type?targetType,?Class<??extends?HttpMessageConverter<?>>?selectedConverterType)?throws?IOException?{return?inputMessage;}@Overridepublic?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;}@Overridepublic?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>?{@Overridepublic?boolean?supports(MethodParameter?returnType,?Class<??extends?HttpMessageConverter<?>>?converterType)?{return?true;}@Overridepublic?Object?beforeBodyWrite(Object?obj,?MethodParameter?returnType,?MediaType?selectedContentType,Class<??extends?HttpMessageConverter<?>>?selectedConverterType,?ServerHttpRequest?serverHttpRequest,ServerHttpResponse?serverHttpResponse)?{//通過?ServerHttpRequest的實現類ServletServerHttpRequest?獲得HttpServletRequestServletServerHttpRequest?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
有道無術,術可成;有術無道,止于術
歡迎大家關注Java之道公眾號
好文章,我在看??
總結
以上是生活随笔為你收集整理的SpringBoot中如何灵活的实现接口数据的加解密功能?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: NYOJ 679 The Weig
- 下一篇: Github 上热门的 Spring B