1. 程式人生 > >解決HttpServletRequest的輸入流只能讀取一次的問題

解決HttpServletRequest的輸入流只能讀取一次的問題

some lar ack implement nis har .post 通過 不支持

背景

通常對安全性有要求的接口都會對請求參數做一些簽名驗證,而我們一般會把驗簽的邏輯統一放到過濾器或攔截器裏,這樣就不用每個接口都去重復編寫驗簽的邏輯。

在一個項目中會有很多的接口,而不同的接口可能接收不同類型的數據,例如表單數據和json數據,表單數據還好說,調用request的getParameterMap就能全部取出來。而json數據就有些麻煩了,因為json數據放在body中,我們需要通過request的輸入流去讀取。

但問題在於request的輸入流只能讀取一次不能重復讀取,所以我們在過濾器或攔截器裏讀取了request的輸入流之後,請求走到controller層時就會報錯。而本文的目的就是介紹如何解決在這種場景下遇到HttpServletRequest的輸入流只能讀取一次的問題。

註:本文代碼基於SpringBoot框架


HttpServletRequest的輸入流只能讀取一次的原因

我們先來看看為什麽HttpServletRequest的輸入流只能讀一次,當我們調用getInputStream()方法獲取輸入流時得到的是一個InputStream對象,而實際類型是ServletInputStream,它繼承於InputStream。

InputStream的read()方法內部有一個postion,標誌當前流被讀取到的位置,每讀取一次,該標誌就會移動一次,如果讀到最後,read()會返回-1,表示已經讀取完了。如果想要重新讀取則需要調用reset()方法,position就會移動到上次調用mark的位置,mark默認是0,所以就能從頭再讀了。調用reset()

方法的前提是已經重寫了reset()方法,當然能否reset也是有條件的,它取決於markSupported()方法是否返回true。

InputStream默認不實現reset(),並且markSupported()默認也是返回false,這一點查看其源碼便知:
技術分享圖片

我們再來看看ServletInputStream,可以看到該類沒有重寫mark()reset()以及markSupported()方法:
技術分享圖片

綜上,InputStream默認不實現reset的相關方法,而ServletInputStream也沒有重寫reset的相關方法,這樣就無法重復讀取流,這就是我們從request對象中獲取的輸入流就只能讀取一次的原因。


使用HttpServletRequestWrapper + Filter解決輸入流不能重復讀取問題

既然ServletInputStream不支持重新讀寫,那麽為什麽不把流讀出來後用容器存儲起來,後面就可以多次利用了。那麽問題就來了,要如何存儲這個流呢?

所幸JavaEE提供了一個 HttpServletRequestWrapper類,從類名也可以知道它是一個http請求包裝器,其基於裝飾者模式實現了HttpServletRequest界面,部分源碼如下:
技術分享圖片

從上圖中的部分源碼可以看到,該類並沒有真正去實現HttpServletRequest的方法,而只是在方法內又去調用HttpServletRequest的方法,所以我們可以通過繼承該類並實現想要重新定義的方法以達到包裝原生HttpServletRequest對象的目的。

首先我們要定義一個容器,將輸入流裏面的數據存儲到這個容器裏,這個容器可以是數組或集合。然後我們重寫getInputStream方法,每次都從這個容器裏讀數據,這樣我們的輸入流就可以讀取任意次了。

具體的實現代碼如下:

package com.example.wrapperdemo.controller.wrapper;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;

/**
 * @author 01
 * @program wrapper-demo
 * @description 包裝HttpServletRequest,目的是讓其輸入流可重復讀
 * @create 2018-12-24 20:48
 * @since 1.0
 **/
@Slf4j
public class RequestWrapper extends HttpServletRequestWrapper {
    /**
     * 存儲body數據的容器
     */
    private final byte[] body;

    public RequestWrapper(HttpServletRequest request) throws IOException {
        super(request);

        // 將body數據存儲起來
        String bodyStr = getBodyString(request);
        body = bodyStr.getBytes(Charset.defaultCharset());
    }

    /**
     * 獲取請求Body
     *
     * @param request request
     * @return String
     */
    public String getBodyString(final ServletRequest request) {
        try {
            return inputStream2String(request.getInputStream());
        } catch (IOException e) {
            log.error("", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 獲取請求Body
     *
     * @return String
     */
    public String getBodyString() {
        final InputStream inputStream = new ByteArrayInputStream(body);

        return inputStream2String(inputStream);
    }

    /**
     * 將inputStream裏的數據讀取出來並轉換成字符串
     *
     * @param inputStream inputStream
     * @return String
     */
    private String inputStream2String(InputStream inputStream) {
        StringBuilder sb = new StringBuilder();
        BufferedReader reader = null;

        try {
            reader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset()));
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            log.error("", e);
            throw new RuntimeException(e);
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    log.error("", e);
                }
            }
        }

        return sb.toString();
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {

        final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);

        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return inputStream.read();
            }

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

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

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

除了要寫一個包裝器外,我們還需要在過濾器裏將原生的HttpServletRequest對象替換成我們的RequestWrapper對象,代碼如下:

package com.example.wrapperdemo.controller.filter;

import com.example.wrapperdemo.controller.wrapper.RequestWrapper;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * @author 01
 * @program wrapper-demo
 * @description 替換HttpServletRequest
 * @create 2018-12-24 21:04
 * @since 1.0
 **/
@Slf4j
public class ReplaceStreamFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("StreamFilter初始化...");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        ServletRequest requestWrapper = new RequestWrapper((HttpServletRequest) request);
        chain.doFilter(requestWrapper, response);
    }

    @Override
    public void destroy() {
        log.info("StreamFilter銷毀...");
    }
}

然後我們就可以在攔截器中愉快的獲取json數據也不慌controller層會報錯了:

package com.example.wrapperdemo.controller.interceptor;

import com.example.wrapperdemo.controller.wrapper.RequestWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

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

/**
 * @author 01
 * @program wrapper-demo
 * @description 簽名攔截器
 * @create 2018-12-24 21:08
 * @since 1.0
 **/
@Slf4j
public class SignatureInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("[preHandle] executing... request uri is {}", request.getRequestURI());
        if (isJson(request)) {
            // 獲取json字符串
            String jsonParam = new RequestWrapper(request).getBodyString();
            log.info("[preHandle] json數據 : {}", jsonParam);

            // 驗簽邏輯...略...
        }

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }

    /**
     * 判斷本次請求的數據類型是否為json
     *
     * @param request request
     * @return boolean
     */
    private boolean isJson(HttpServletRequest request) {
        if (request.getContentType() != null) {
            return request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE) ||
                    request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE);
        }

        return false;
    }
}

編寫完以上的代碼後,還需要將過濾器和攔截器在配置類中進行註冊才會生效,過濾器配置類代碼如下:

package com.example.wrapperdemo.config;

import com.example.wrapperdemo.controller.filter.ReplaceStreamFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;

/**
 * @author 01
 * @program wrapper-demo
 * @description 過濾器配置類
 * @create 2018-12-24 21:06
 * @since 1.0
 **/
@Configuration
public class FilterConfig {
    /**
     * 註冊過濾器
     *
     * @return FilterRegistrationBean
     */
    @Bean
    public FilterRegistrationBean someFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(replaceStreamFilter());
        registration.addUrlPatterns("/*");
        registration.setName("streamFilter");
        return registration;
    }

    /**
     * 實例化StreamFilter
     *
     * @return Filter
     */
    @Bean(name = "replaceStreamFilter")
    public Filter replaceStreamFilter() {
        return new ReplaceStreamFilter();
    }
}

攔截器配置類代碼如下:

package com.example.wrapperdemo.config;

import com.example.wrapperdemo.controller.interceptor.SignatureInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author 01
 * @program wrapper-demo
 * @description
 * @create 2018-12-24 21:16
 * @since 1.0
 **/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Bean
    public SignatureInterceptor getSignatureInterceptor(){
        return new SignatureInterceptor();
    }

    /**
     * 註冊攔截器
     *
     * @param registry registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(getSignatureInterceptor())
                .addPathPatterns("/**");
    }
}

接下來我們就可以測試一下在攔截器中讀取了輸入流後在controller層是否還能正常接收數據,首先定義一個實體類,代碼如下:

package com.example.wrapperdemo.param;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author 01
 * @program wrapper-demo
 * @description
 * @create 2018-12-24 21:11
 * @since 1.0
 **/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserParam {
    private String userName;

    private String phone;

    private String password;
}

然後寫一個簡單的Controller,代碼如下:

package com.example.wrapperdemo.controller;

import com.example.wrapperdemo.param.UserParam;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 01
 * @program wrapper-demo
 * @description
 * @create 2018-12-24 20:47
 * @since 1.0
 **/
@RestController
@RequestMapping("/user")
public class DemoController {

    @PostMapping("/register")
    public UserParam register(@RequestBody UserParam userParam){
        return userParam;
    }
}

啟動項目,請求結果如下,可以看到controller正常接收到數據並返回了:
技術分享圖片

控制臺輸出如下:
技術分享圖片

解決HttpServletRequest的輸入流只能讀取一次的問題