java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > springboot接口加签验签常见问题及解决

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

作者:路西法_Lucifer

在SpringBoot框架中通过自定义注解实现加签验签功能是一个非常实用的技术,本文主要介绍了使用SpringBoot进行加签验签时可能遇到的几个问题,包括请求流重复读取问题、控制器中文件参数读取为空问题、FormData表单提交MD5加密值不一致问题

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()为空;

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

解决办法: 将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
            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.getInputStream();
        String digestHex = MD5.create().digestHex(IoUtil.readBytes(inputStream));
        log.info("============:{}", digestHex);
        Object obj = joinPoint.proceed();
        return obj;
    }

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

问题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这个参数居然读不到,显示为空

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

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

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

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

}

效果如下:

问题3

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

问题分析:

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

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

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

解决办法:

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

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

办法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.MULTIPART.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(); 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.create().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是否一致:

    @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的内容不一样,导致生成的签名跟我这边的始终不一样。

解决办法:

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文