密码保护:接口平台防重
API防重攻击
预告:ThreadLocal的探索
预告:心跳机制的实现
ServletRequest中的getReader() 和getInputStream()只能调用一次的问题探究
起因:
在项目中做对外接口API防重设计
的时候,发现项目有个类 XXXRequestWrapper
,这个类继承 HttpServletRequestWrapper
并重写了 getInputStream()
和getReader()
方法,代码示例如下:
public class XXXRequestWrapper extends HttpServletRequestWrapper{
//省略constrcutor以及自定义filed
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
}
@Override
public ServletInputStream getInputStream() {
//省略实现代码
}
项目中自定义的Filter中都会把 servletRequest转化为 该warpper,让我十分不解,一度认为这是脱裤子放屁,多此一举的行为。然鹅,最后被打脸的确是我自己……
一探:
经过搜索后得知,因为ServletRequest
中的getInputStream()
/getReader()
只能被调用一次 而一个项目中肯定存在多处需要调用该方法获取流中数据的场景,所以需要自定义一个XXXWrapper类 并重写getReader()
和getInputStream()
方法,将流中数据保存并向下传递(其他Filter).伪代码如下:
public class CustomizeFilter implements Filter{
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
if(request instanceof HttpServletRequest) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
requestWrapper = new XXXWrapper(request)
}
if(requestWrapper == null) {
chain.doFilter(request, response);
} else {
chain.doFilter(requestWrapper, response); //将requet 替换requestWrapper向下传递
}
}
}
此种做法可以避免之前提到的流只能被读取一次的问题。但是仍然有个问题困扰着我,为什么只能被读取一次?这个问题,我搜了下,其实也有不少人研究过,有些给的答案很形象,你可以把流中的数据当作一瓢水,你调用getInputStream方法就相当于从瓢中取水,取了一次以后,自然就没有了。坦白说,看到这个解释的时候,我恍然大悟,好像明白了,又好像什么都没明白。因为,这个答案没有从根本上说明答案。
二探:
ServletRequest中的getInputStream()为什么只能调用一次? 这个问题还是让我有些疑惑,我继续寻找答案,终于,一番努力(百度)下 ,找到了想要的答案。
其实这个问题最终归于I/O的读取,我们可以先看下ServletRequest的getInputStream()是怎样的,源码如下:
/**
* 省略
*/
public ServletInputStream getInputStream() throws IOException;
其返回值为ServletInputStream
而ServletInputStream
又是怎样的呢?
public abstract class ServletInputStream extends InputStream {
/**
* Does nothing, because this is an abstract class.
*/
protected ServletInputStream() {
// NOOP
}
//省略其余部分
}
由此我们可以看到 ServletInputStream
其实是 InputStream
的子类,而且该方法的实现上来看,
getInputStream()
方法 最终是由RequestFacade类实现的,其实现源码为:
@Override
public ServletInputStream getInputStream() throws IOException {
if (request == null) {
throw new IllegalStateException(
sm.getString("requestFacade.nullRequest"));
}
return request.getInputStream();
}
其中的 request.getInputStream() 则是由 Request实现的:
@Override
public ServletInputStream getInputStream() throws IOException {
if (usingReader) {
throw new IllegalStateException(sm.getString("coyoteRequest.getInputStream.ise"));
}
usingInputStream = true;
if (inputStream == null) {
inputStream = new CoyoteInputStream(inputBuffer);
}
return inputStream;
}
再往下探究其实已经没必要了,因为ServletRequest 的 getInputStream()
返回的为InputStream的子类,而我们在读取该流的时候(即在Filter中获取request
中的参数时),其使用的是InputStream的read
方法.
对于InputStream的read
方法:
/**
* Reads up to <code>len</code> bytes of data from the input stream into
* an array of bytes. An attempt is made to read as many as
* <code>len</code> bytes, but a smaller number may be read.
* The number of bytes actually read is returned as an integer.
*
* <p> This method blocks until input data is available, end of file is
* detected, or an exception is thrown.
*
* <p> If <code>len</code> is zero, then no bytes are read and
* <code>0</code> is returned; otherwise, there is an attempt to read at
* least one byte. If no byte is available because the stream is at end of
* file, the value <code>-1</code> is returned; otherwise, at least one
* byte is read and stored into <code>b</code>.
**/
public int read(byte b[], int off, int len) throws IOException {
//省略实现
}
关于read()方法,通俗的讲,就是每次读取都会记录该位置pos,下次读取,则从该pos+1的位置读取流中数据,直到读取到文件或者流的结束位置返回-1,而我们在Filter中调用getInputStream()
则是相当于将流中数据全部读取了,使得pos 指向文件/流末尾,再次
读取自然不能获取到流中数据,聊到这,我估计有些人会有跟我一样的想法,既然是pos指针问题,那重置该pos的值不就可以解决问题了嘛,是的 , InputStream提供了reset()
方法用来重置pos指针,然鹅,然鹅,然鹅,我们还是看下源码吧。
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
直接抛出IO异常,查了下资料,有解释说,Java期望有需要重写该方法的子类,自己去重写该方法,只需要遵循方法合约(可自行查看该方法说明)即可。
在其子类里翻了 下,确实有重写了该方法的,如 BufferedInputStream
public synchronized void reset() throws IOException {
getBufIfOpen(); // Cause exception if closed
if (markpos < 0)
throw new IOException("Resetting to invalid mark");
pos = markpos;
}
那么我们上文提到的 ServletInputStream呢 ,很不巧,其并未重写该方法。
所以,这也就导致我们想要在读取完之后重置pos的想法落空,只能另辟蹊径,采用文章开头介绍的方式了。
三探:
关于I/O的流读取问题,其实也做一些测试,但是发掘不如网上的文章说的详细,这里也懒得赘述了,链接在此,有兴趣的可以去看下。