SpringBoot中如何灵活的实现接口数据的加解密功能?
数据是企业的第四张名片,企业级开发中少不了数据的加密传输,所以本文介绍下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实现类的部分代码如下:
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);
@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中如何灵活的实现接口数据的加解密功能的功能已经全部实现,有问题欢迎留言沟通哦!
完整源码地址:
推荐阅读
1.springboot中神奇的@enable*注解?
2.java中integer.parseint和integer.valueof,你还傻傻分不清吗?
3.springcloud系列-整合hystrix的两种方式
4.springcloud系列-利用feign实现声明式服务调用
5.手把手带你利用ribbon实现客户端的负载均衡
限时领取免费java相关资料,涵盖了java、redis、mongodb、mysql、zookeeper、spring cloud、dubbo/kafka、hadoop、hbase、flink等高并发分布式、大数据、机器学习等技术。
关注下方公众号即可免费领取:
上一篇: 微信小程序中进行地图导航功能的实现方法
下一篇: 详解浏览器缓存和webpack缓存配置