开发者

springboot接口加签验签常见的几大问题及解决过程

开发者 https://www.devze.com 2024-11-06 10:20 出处:网络 作者: 路西法_Lucifer
目录springboot接口加签验签常见问题及解决1、测试Controller2、aop切面问题1问题2问题3问题4(额外拓展)总结springboot接口加签验签常见问题及解决
目录
  • springboot接口加签验签常见问题及解决
    • 1、测试Controller
    • 2、aop切面
    • 问题1
    • 问题2
    • 问题3
    • 问题4(额外拓展)
  • 总结

    springboot接口加签验签常见问题及解决

    ps:通过springboot自定义注解实现验签的加签验签功能,不过很多容易遇坑的地方.

    所需pom.XML中的jar包:

            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
                <version>5.8.0</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
            <dependency>
                <groupId>commons-fileupload</groupId>
                <artifactId>commons-fileupload</artifactId>
                <version>1.4</version>
            </dependency>

    原始代码段:

    1、测试Controller

    @RestController
    @RequestMapping
    public class TestController {
    
        @PostMapping("test1")
        public void test1(MultipartFile file){
            System.out.println("...........");
        }
    
    
        @PostMapping("test2")
        public void test2(@RequestBody String str){
            System.out.println("...........");
        }
    
    }

    2、aop切面

    (ps: 这里关于通过自定义注解实现验签加签的代码就不写了,其实很简单,这里主要是为了说明可能遇到的问题,以及解决办法,至于加签验签的具体代码,自行百度)

    @Slf4j
    @Component
    @ASPect
    public class TestAop {
    
        @Around(value = "execution(* com.example.demo.controller.*.*(..))")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            ServletInputStream inputStream = request.getInputStream();
            //对请求体得到的字节进行加密,我的加签是通过url后面的一些参数+AccessKey+body+secreteKey等这些字段进行加密,我会对body加密后,然后跟其它加签字段拼接起来,然后MD5得到签名的,具体加签规则去百度
            //这里我就为了演示,只对body加密了
            String digestHex = MD5.create().digestHex(IoUtil.readBytes(inputStream));
            log.info("============:{}", digestHex);
            Object obj = joinPoint.proceed();
            return obj;
        }
    }

    可能产生的问题

    问题1

    1.1 因为request.getInputStream()读取的流,读到一次之后,就会关掉,所以过滤器中获取request.getInputStream()为空;

    springboot接口加签验签常见的几大问题及解决过程

    1.2 如果对流获取多次,就会出现异常,request.getInputStream()读取的流,读到一次之后,就会关掉。

    springboot接口加签验签常见的几大问题及解决过程

    解决办法: 将request进行包装,让request.getInputStream()可以重复读取

    1. RequestWrapper 对request对象进行包装

    package com.example.demo.config;
    
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.compress.utils.IOUtils;
    
    import Javax.servlet.ReadListener;
    import javax.servlet.ServletInputStream;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletRequestWrapper;
    import java.io.*;
    
    @Slf4j
    public class RequestWrapper extends HttpServletRequestWrapper {
    
        private ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    
        public RequestWrapper(HttpServletRequest request) throws IOException {
            super(request);
            IOUtils.copy(request.getInputStream(), byteArrayOutputStream);
        }
    
        @Override
        public ServletInputStream getInputStream() throws IOException {
            if (byteArrayOutputStream == null) {
                IOUtils.copy(super.getInputStream(), byteArrayOutputStream);
            }
            return new ServletInputStream() {
                private final ByteArrayInputStream input = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
    
                @Override
                public boolean isFinished() {
                    return false;
                }
    
                @Override
                public boolean isReady() {
                    return false;
                }
    
                @Override
           php     public void setReadListener(ReadListener readListener) {
    
                }
    
                @Override
                public int read() throws IOException {
                    return input.read();
                }
            };
        }
    
        @Override
        public BufferedReader getReader() throws IOException {
            return new BufferedReader(new InputStreamReader(getInputStream()));
        }
    
    
        public ByteArrayOutputStream getByteArrayOutputStream() {
            return byteArrayOutputStream;
        }
    
    }
    

    2. RequestWrapperFilter 过滤器 让request变成自己的request包装对象

    package com.example.demo.config;
    
    import lombok.extern.slf4j.Slf4j;
    
    import javax.servlet.*;
    import javax.servlet.annotation.WebFilter;
    import javax.servlet.http.HttpServletRequest;
    import java.io.IOException;
    
    //filter 过滤器   urlPatterns自己设置过滤的路径,这里为了演示,就怎么方便怎么来了
    @WebFilter(urlPatterns = "/*")
    @Slf4j
    public class RequestWrapperFilter implements Filter {
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            ServletRequest requestWrapper = null;
            if (request instanceof HttpServletRequest) {
                requestWrapper = new RequestWrapper((HttpServletRequest) request);
            }
            if (requestWrapper == null) {
                chain.doFilter(request, response);
            } else {
                chain.doFilter(requestWrapper, response);
            }
        }
    }

    3. springboot的启动类中加入@ServletComponentScan,让filter生效

    @SpringBootApplication
    @ServletComponentScan
    public class DemoApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    
    }

    4、修改aop,不再读取直接读取request,而是读取new RequestWrapper(request)对象

        @Around(value = "execution(* com.example.demo.controller.*.*(..))")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            RequestWrapper requestWrapper = new RequestWrapper(request);
            ByteArrayOutputStream byteArrayOutputStream = requestWrapper.getByteArrayOutputStream();
    
            ServletInputStream inputStream = requestWrapper.getI编程nputStream();
            String digestHex = MD5.create().digestHex(IoUtil.readBytes(inputStream));
            log.info("============:{}", digestHex);
            Object obj = joinPoint.proceed();
            return obj;
        }

    验证结果:(多次读取getIntPutStream没有问题)

    springboot接口加签验签常见的几大问题及解决过程

    问题2

    aop切面读取到的文件是有值,但是controller接口中file这个参数居然读不到,显示为空

    修改后的aop代码段:

       @Around(value = "execution(* com.example.demo.controller.*.*(..))")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            RequestWrapper requestWrapper = new RequestWrapper(request);
            ByteArrayOutputStream byteArrayOutputStream = requestWrapper.getByteArrayOutputStream();
            String digestHex = MD5.create().digestHex(byteArrayOutputStream.toByteArray());
            log.info("============:{}", digestHex);
            Object obj = joinPoint.proceed();
            return obj;
        }

    当调用接口test2时,控制台打印如下:

    2022-10-14 21:32:56.207  INFO 9968 --- [nio-8080-exec-1] com.example.demo.aop.TestAop             : ============:ab2ee64ec2b80edc3553a826c4610733

    ...........testEntity:{"id":1,"username":"zhangsan"}

    2022-10-14 21:32:59.636  INFO 9968 --- [nio-8080-exec-3] com.example.demo.aop.TestAop             : ============:ab2ee64ec2b80edc3553a826c4610733

    ...........testEntity:{"id":1,"username":"zhangsan"}

    当调用接口test1时,如图:

    aop切面读取到的文件是有值,但是controller接口中file这个参数居然读不到,显示为空

    springboot接口加签验签常见的几大问题及解决过程

    springboot接口加签验签常见的几大问题及解决过程

    问题出在哪儿呢??????

    只要是http请求都包装下request对象,最终用的都是requestWrapper对象。

    springboot接口加签验签常见的几大问题及解决过程

    当我调用test2接口,body是实体类对象,是可以接收参数的;

    springboot接口加签验签常见的几大问题及解决过程

    ps: 使用requestWrapper对象解决了输入流的重复读取的问题,但是却引发了接口文件读取为空的bug.对test2接口,这种用实体对象接收,没有问题。

    解决办法:替换springboot对file的默认实现,改用commons-fileupload包的

        <dependency>
                <groupId>commons-fileupload</groupId>
                <artifactId>commons-fileupload</artifactId>
                <version>1.4</version>
            </dependency>

    排除MultipartAutoConfiguration的默认实现,改用CommonsMultipartResolver

    @SpringBootApplication(exclude = MultipartAutoConfiguration.class)
    @ServletComponentScan
    public class DemoApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    
        
        @Bean(name = "multipartResolver")
        public MultipartResolver multipartResolver() {
            CommonsMultipartResolver resolver = new CommonsMultipartResolver();
            resolver.setDefaultEncoding("UTF-8");
            return resolver;
        }
    
    }

    效果如下:

    springboot接口加签验签常见的几大问题及解决过程

    问题3

    form-data 表单提交,对body的字节数组进行MD5加密,相同请求,相同参数,生成的MD5值每次却不一样,对于接口参数是MultipartFile参数而言。

    springboot接口加签验签常见的几大问题及解决过程

    问题分析:

    如果调用test2接口,则通过输入流读取的内容是一个json,并没有添加请求头以及其它额外部分。

    springboot接口加签验签常见的几大问题及解决过程

    如果调用test1接口,则通过输入流读取的内容是一堆乱码,并添加请求头以及其它额外部分。

    springboot接口加签验签常见的几大问题及解决过程

    springboot接口加签验签常见的几大问题及解决过程

    绿色框的是如果接口参数是文件的话,获取输入流,会在流里添加除了文件内容之外,还会额外添加的部分,而红色框住的部分,每次都不一样,因此尽管每次相同请求,相同文件,这个输入流读取为字节数组后,都是不一样的,因此MD5加密得到的MD5值也肯定不一样。

    解决办法:

    因此不应该直接读取reque.getInputStream()里面的内容,应该拿到这个流后,对它应该是文件内容的其它额外添加的如请求头,随机数之类的东西全部去掉后,拿到单纯的只是文件的内容。

    办法1:不用form-data提交,改用binary 二进制流提交,这种提交,request.getInputStream()得到的流 里面不会添加除了文件内容外,如请求头、随机数等额外部分。

    springboot接口加签验签常见的几大问题及解决过程

    办法2:仍然使用form-data提交方式,将request.getInputStream()得到的流 里面添加除了文件内容外,如请求头、随机数等额外部分都去掉。

    修改后的RequestWrapper :

    package com.example.demo.config;
    
    import cn.hutool.core.collection.CollUtil;
    import cn.hutool.http.ContentType;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.compress.utils.IOUtils;
    import org.apache.commons.fileupload.FileItem;
    import org.apache.commons.fileupload.FileUploadException;
    import org.apache.commons.fileupload.disk.DiskFileItemFactory;
    import org.apache.commons.fileupload.servlet.ServletFileUpload;
    
    import javax.servlet.ReadListener;
    import javax.servlet.ServletInputStream;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletRequestWrapper;
    import java.io.*;
    import java.util.List;
    
    @Slf4j
    public class RequestWrapper extends HttpServletRequestWrapper {
    
        private ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    
        public RequestWrapper(HttpServletRequest request) throws IOException {
            super(request);
            IOUtils.copy(request.getInputStream(), byteArrayOutputStream);
        }
    
        @Override
        public ServletInputStream getInputStream() throws IOException {
            if (byteArrayOutputStream == null) {
                IOUtils.copy(super.getInputStream(), byteArrayOutputStream);
            }
            return new ServletInputStream() {
                private final ByteArrayInputStream input = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
    
                @Override
                public boolean isFinished() {
                    return false;
                }
    
                @Override
                public boolean isReady() {
                    return false;
                }
    
                @Override
                public void setReadListener(ReadListener readListener) {
    
                }
    
                @Override
                public int read() throws IOException {
                    return input.read();
                }
            };
        }
    
        @Override
        public BufferedReader getReader() throws IOException {
            return new BufferedReader(new InputStreamReader(getInputStream()));
        }
    
    
        public ByteArrayOutputStream getByteArrayOutputStream() {
            return byteArrayOutputStream;
        }
    
        public byte[] getPureBody() throws FileUploadException {
            if (this.getContentType().contains(ContentType.MULTpythonIPART.getValue())) {
                DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();
                ServletFileUpload servletFileUpload = new ServletFileUpload(diskFileItemFactory);
                //获取所有的上传文件
                List<FileItem> fileItems = servletFileUpload.parseRequest(this);
                if (CollUtil.isNotEmpty(fileItems)) {
                    byte[][] bytes = new byte[fileItems.size()][];
                    for (int i = 0; i < fileItems.size();js i++) {
                        bytes[i] = fileItems.get(i).get();
                    }
                    //判断二维数组不能为空
                    if (bytes != null && bytes.length > 0) {
                        if (!(bytes.length == 1 && bytes[0].length == 0)) {
                            //将多个文件对应的字节数组合并成一个字节数组 byte[]
                            return mergeBytes(bytes);
                        }
                    }
                }
            }
            return byteArrayOutputStream.toByteArray();
        }
    
        private static byte[] mergeBytes(byte[]... values) {
            int lengthByte = 0;
            for (byte[] value : values) {
                lengthByte += value.length;
            }
            byte[] allBytes = new byte[lengthByte];
            int countLength = 0;
            for (byte[] b : values) {
                System.arraycopy(b, 0, allBytes, countLength, b.length);
                countLength += b.length;
            }
            return allBytes;
        }
    
    
    }
    

    修改后的aop:

    package com.example.demo.aop;
    
    import cn.hutool.core.io.IoUtil;
    import cn.hutool.crypto.digest.MD5;
    import com.example.demo.config.RequestWrapper;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    import javax.servlet.http.HttpServletRequest;
    
    @Slf4j
    @Component
    @Aspect
    public class TestAop {
    
        @Around(value = "execution(* com.example.demo.controller.*.*(..))")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            RequestWrapper requestWrapper = new RequestWrapper(request);
            String digestHex = MD5.crejavascriptate().digestHex(requestWrapper.getPureBody());
            log.info("============:{}", digestHex);
            Object obj = joinPoint.proceed();
            return obj;
        }
    }

    测试结果:

    调用两次接口test1,查看控制台

    2022-10-14 22:29:07.548 xxTestAop: =====:be53fbee299d4c13b4f68420afb26127

    ...........jd-gui.exe

    2022-10-14 22:29:08.719 x.TestAop: =====:be53fbee299d4c13b4f68420afb26127

    调用两次接口test2,查看控制台

    2022-10-14 22:29:10.903   TestAop: ====:ab2ee64ec2b80edc3553a826c4610733

    ...........testEntity:{"id":1,"username":"zhangsan"}

    2022-10-14 22:29:11.762  TestAop:=====:ab2ee64ec2b80edc3553a826c4610733

    增加接口test3,验证多文件上传,生成的MD5是否一致:

    springboot接口加签验签常见的几大问题及解决过程

        @PostMapping("test3")
        public void test3(List<MultipartFile> file) {
            for (MultipartFile multipartFile : file) {
                System.out.println("..........." + multipartFile.getOriginalFilename());
            }
        }

    控制台输出:

    2022-10-14 22:38:47.764  TestAop ===:7a0f9ab5a674935ff8a5177a49c0efdf

    ...........jd-gui.exe

    ...........README.md

    2022-10-14 22:38:54.506  TestAop ====:7a0f9ab5a674935ff8a5177a49c0efdf

    ...........jd-gui.exe

    ...........README.md

    问题4(额外拓展)

    不是本博客的代码出现的,曾经也是提供给第三方调用的接口加签名验签,由于那次是第一次写接口的加签验签功能,接口没有文件上传,全是传json的这种,但是当时怎么做得呢?

    我拿到的body是一个实体类的对象,我用fastjson对它进行解析,得到json字符串,然后跟其它需要加签的字段拼接在一起,在这里有一个问题的,但是当时自己自测,没有测出来,因为我postman里面的json中属性位置是一样的,而调用者的body里面属性位置跟我不一样,最终body的内容不一样,导致生成的签名跟我这边的始终不一样。

    解决办法:

    • 方法1:提供相同的json解析工具,确保两边的body的json串里面属性在json解析后,顺序是一样的
    • 方法2:读取输入流,接口传参是什么样子,自己拿到的就是什么样子,可以对输入流进行操作,也可以对从输入流中读取到字节数组操作

    总结

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

    0

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    关注公众号