场景描述:
要对请求数据,进行通用的XSS规则验证,所以需要在请求进入到具体的Controller的接口前,进行拦截处理.
在Context-Type=application/json的请求中,要对请求参数进行验证,须从body中的流中去获取数据,但是从流中读取数据只能读取一次.
读取数据:
在InputStream的read方法内部,存在position,来标识当前流被读取到的位置,读取到哪里就标志到哪里.如果读到最后,那么就返回-1.
如果想要重新读取流中的数据,那么就需要调用reset方法,那么position就会重置到上次调用mark的位置,而mark默认位置是0.就可以从头在读.
调用reset的方法的前提是:
1.markSupported 方法返回值必须为true.
2.必须重写reset方法
只读一次:
现在需要确定,为什么从body的流中读取数据只能读取一次呢?
既然已知读取流中数据的原理,以及如何重复读取流中数据的前提,那么我们来查看请求中获取流的方法
具体从request中获取流的方法:
ServletInputStream steam = request.getInputStream();
首先,我们来分析ServletInputStream源码:
public abstract class ServletInputStream extends InputStream
1.ServletInputStream继承自InputStream类
2.ServletInputStream没有重写reset方法和markSupported 方法
其次,查看其分类是否重写reset和markSupported 方法
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
public boolean markSupported() {
return false;
}
查看InputStream类可知,没有重写reset方法和markSupported 方法.因此从ServletInputStream steam = request.getInputStream();只能从ServletInputStream 读取流中的数据一次.
具体我们查看InputStream的API.具体中文版如下:
https://www.matools.com/file/manual/jdk_api_1.8_google/java/io/InputStream.html
读取多次:
如何获取流中的数据多次呢?
前提是ServletInputStream steam = request.getInputStream();去获取流中的数据只能获取一次.而根据面向对象的多态特性,凡是父类出现的地方都可以用子类替换掉,调用的方法都可以使用子类重写后的方法.
从Filter中方法可知:传递的是ServletRequest接口的实现类
我们可以重写该接口的实现类,然后传入到doFilter方法中.
javax.servlet.HttpServletRequestWrapper已实现 HttpServletRequest
public class HttpServletRequestWrapper extends
ServletRequestWrapper implements HttpServletRequest
因此我们只需要自定义个包装类,然后将从流中的数据保存到自定义的数组中或字符串中,然后重写getInputStream方法即可.
package com.neutron.request.filter; 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; @Slf4j public class XssRequestWrapper extends HttpServletRequestWrapper { /** 保存获取IO流中数据,以后从body中获取流数据 */ private final byte[] body;
public XssRequestWrapper(HttpServletRequest request) { super(request); // 将body中数据存储起来 String stream = getBodyString(request); body = stream.getBytes(Charset.forName("UTF-8")); } /** 获取请求Body */ private String getBodyString(final ServletRequest request) { try { return inputStream2String(request.getInputStream()); } catch (IOException e) { throw new RuntimeException(e); } } /** 获取请求Body */ public String getBodyString() { final InputStream inputStream = new ByteArrayInputStream(body); return inputStream2String(inputStream); } /** 将inputStream里的数据读取出来并转换成字符串 */ 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 stream = new ByteArrayInputStream(body); return new ServletInputStream() { @Override public int read() throws IOException { return stream.read();} @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } }; } }
将自定义的包装类,向后面的接口做参数传递,那么相当于可以从流中多次获取数据.实际上已经从流中获取数据一次,存放在某个缓存中,以后获取流中的数据都从缓存中获取.
否则从流读取完数据后,经过拦截器和过滤器后,映射到具体的接口时,比如:
xss对象的数据会直接为null,不会经过字段映射直接赋值.
参考地址: https://blog.csdn.net/qq_34548229/article/details/104014374
项目代码: https://github.com/zhtzyh2012/io-flow2 (已做验证,可使用)