如何在SpringBoot的Request中多次读取InputStream

最近一个项目需要在请求中效验签名 签名的参数用到了请求的数据
如果请求的数据是 application/x-www-form-urlencoded 的格式那么没问题
但是如果请求的数据是 application/json 则用到了 @RequestBody 中的数据
但是由于 InputStream 只能取一次数据
最后导致 Controller 中取不到数据

解决方案

  • 新建一个 SignHttpServletRequestWrapper 包装请求

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    package com.sixi.enquiry.interceptor;

    import java.io.BufferedInputStream;
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.io.InputStream;

    import javax.servlet.ReadListener;
    import javax.servlet.ServletInputStream;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletRequestWrapper;

    /**
    * Created with IntelliJ IDEA
    *
    * @author 喵♂呜
    * Created on 2017/12/20 19:01.
    */
    public class SignHttpServletRequestWrapper extends HttpServletRequestWrapper {
    private final byte[] body;

    /**
    * Constructs a request object wrapping the given request.
    *
    * @param request
    * The request to wrap
    * @throws IllegalArgumentException
    * if the request is null
    */
    public SignHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
    super(request);
    body = readBytes(request.getInputStream());
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
    final ByteArrayInputStream bais = new ByteArrayInputStream(body);
    return new ServletInputStream() {

    @Override
    public int read() throws IOException {
    return bais.read();
    }

    @Override
    public boolean isFinished() {
    return bais.available() == 0;
    }

    @Override
    public boolean isReady() {
    return false;
    }

    @Override
    public void setReadListener(ReadListener arg0) {
    }
    };
    }

    private byte[] readBytes(InputStream in) throws IOException {
    int buffSize = 1024;
    try (BufferedInputStream bis = new BufferedInputStream(in); ByteArrayOutputStream out = new ByteArrayOutputStream(buffSize)) {
    byte[] temp = new byte[buffSize];
    int size;
    while ((size = bis.read(temp)) != -1) {
    out.write(temp, 0, size);
    }
    return out.toByteArray();
    }
    }
    }
  • 添加一个 SignFilter 过滤器用来封装请求

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    package com.sixi.enquiry.interceptor;

    import java.io.IOException;

    import javax.servlet.Filter;
    import javax.servlet.FilterChain;
    import javax.servlet.FilterConfig;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;

    import org.springframework.http.HttpMethod;
    import org.springframework.http.MediaType;

    /**
    * Created with IntelliJ IDEA
    *
    * @author 喵♂呜
    * Created on 2017/12/20 18:58.
    */
    public class SignFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    if (request instanceof HttpServletRequest) {
    HttpServletRequest httpServletRequest = (HttpServletRequest) request;
    // 只处理POST并且ContentType是JSON的请求 这里根据业务情况自行修改
    if (HttpMethod.POST.matches(httpServletRequest.getMethod().toUpperCase()) &&
    httpServletRequest.getContentType().contains(MediaType.APPLICATION_JSON_VALUE)) {
    ServletRequest requestWrapper = new SignHttpServletRequestWrapper((HttpServletRequest) request);
    chain.doFilter(requestWrapper, response);
    return;
    }
    }
    chain.doFilter(request, response);
    }

    @Override
    public void destroy() {}
    }
  • 在配置类内新增一个 Bean 注册过滤器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Bean
    @Order
    public FilterRegistrationBean SignFilterRegistration() {
    FilterRegistrationBean registration = new FilterRegistrationBean();
    registration.setFilter(new SignFilter());
    registration.addUrlPatterns("/*");
    registration.setName("SignFilter");
    registration.setOrder(Integer.MAX_VALUE);
    return registration;
    }
  • 新增一个用到了 getInputStream 的拦截器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package com.sixi.enquiry.interceptor;

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;

    import org.springframework.stereotype.Component;
    import org.springframework.util.StreamUtils;
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

    /**
    * @author 喵♂呜
    * @since 2017/5/31
    */
    @Component
    public class SignInterceptor extends HandlerInterceptorAdapter {
    private String SignHeader = "Sixi-Signature";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
    StreamUtils.copyToString(request.getInputStream(), Charset.forName("UTF-8"));
    return true;
    }
    }
  • 新增控制器 打印数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    package com.sixi.enquiry.controller;

    import java.io.IOException;
    import java.nio.charset.Charset;

    import javax.servlet.http.HttpServletRequest;

    import org.springframework.util.StreamUtils;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    /**
    * Created with IntelliJ IDEA
    *
    * @author 喵♂呜
    * Created on 2017/12/21 19:04.
    */
    @RestController
    @RequestMapping("/test")
    public class TestController {
    @PostMapping
    public String test(HttpServletRequest request) throws IOException {
    return StreamUtils.copyToString(request.getInputStream(), Charset.forName("UTF-8"));
    }
    }
  • 使用CURL测试 可以获得数据

    1
    2
    3
    4
    $ curl -sX POST http://172.30.34.3:1221/test \
    > -H 'Content-Type: application/json' \
    > -d '{"id":111,"userId":1,"amount":100,"products":[{"id":123,"name":"testproduct","price":23}]}'
    {"id":111,"userId":1,"amount":100,"products":[{"id":123,"name":"testproduct","price":23}]}