欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

SpringBoot中如何灵活的实现接口数据的加解密功能?

程序员文章站 2024-01-21 20:57:46
数据是企业的第四张名片,企业级开发中少不了数据的加密传输,所以本文介绍下SpringBoot中接口数据加密、解密的方式。 本文目录 一、加密方案介绍二、实现原理三、实战四、测试五、踩到的坑 一、加密方案介绍 对接口的加密解密操作主要有下面两种方式: 自定义消息转换器 优势:仅需实现接口,配置简单。劣 ......

数据是企业的第四张名片,企业级开发中少不了数据的加密传输,所以本文介绍下springboot中接口数据加密、解密的方式。

本文目录

一、加密方案介绍

对接口的加密解密操作主要有下面两种方式:

  1. 自定义消息转换器

优势:仅需实现接口,配置简单。
劣势:仅能对同一类型的mediatype进行加解密操作,不灵活。

  1. 使用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,按照下面步骤操作。

  1. 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>
  1. 请求参数解密拦截类

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;
    }


}
  1. 响应参数加密拦截类

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;
    }
  1. 新建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;
    }
}
  1. 其他类在源码中,后面有github地址

四、测试

  1. 访问响应数据加密接口

使用postman发请求http://localhost:8888/sendresponseencrydata,可以看到返回数据已加密,请求截图如下:

SpringBoot中如何灵活的实现接口数据的加解密功能?
响应数据加密截图

后台也打印相关的日志,内容如下:

接口=/sendresponseencrydata

原始数据={"data":{"encry":true,"name":"java碎碎念"},"success":true}

加密后数据=vjc26g3sqru9gajdg7rhnax6ky/ihgioagdwi6almmtyynab4nebmxvdskepnia5bqat7zaimal7
3veiccusta==
  1. 访问请求数据解密接口

使用postman发请求http://localhost:8888/getrequestdata,可以看到请求数据已解密,请求截图如下:

SpringBoot中如何灵活的实现接口数据的加解密功能?
请求数据解密截图

后台也打印相关的日志,内容如下:

接收到原始请求数据={"data":"vwlvde8n6fusxn/jrrjavatopaba3m1qen+9bkuf2jpwc1esofgahq=="}

解密后数据={"name":"java碎碎念","des":"请求参数"}

五、踩到的坑

  1. 测试解密请求参数时候,请求体一定要有数据,否则不会调用实现类触发解密操作。

到此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等高并发分布式、大数据、机器学习等技术。
关注下方公众号即可免费领取:

SpringBoot中如何灵活的实现接口数据的加解密功能?