遇到一个需求,tomcat本身在日志中记录POST请求需要修改配置,要求使用其他手段获取到http请求和相应的data,header等,这时候想到了类似filter内存马的思路,filter内存马本身就是注册一个filter,拦截请求,处理并返回结果。
因为这次需要获取response的内容,先学习一下tomcat的filter链。
filter的责任链模式
filter可以同时过滤请求和响应,在tomcat的中所处的位置:
当一个消息(包含请求体和响应体)发往服务器时, 它将依次经过过滤器1, 2, 3. 而当处理完成后, 封装好响应发出服务器时, 它也将依次经过过滤器3, 2, 1。
在过滤器代码中有一个标准函数:
// 处理request
...
filterchain.doFilter(request, response);
// 处理response
...
在这个函数前的部分,一般用来处理request,调用doFilter
之后,请求会继续发给下一个filter,直到所有过滤器完成,交给servlet处理。处理完后就有response了,再按照反过来的过滤器顺序依次执行doFilter
后面那部分代码,一般用来处理响应。
但是有些请求的response主要内容是在filter中添加的,为了保证能获取到完整的response,要把filter放在链的第一个,一般是fitlermap的第一位。
tomcat大概体系结构
这部分涉及到filter servlet等核心组件的生效范围,简单看一下
顶层server,代表服务器,一个Server可以至少包含一个service,用于提供服务
service主要包含两个部分Connector和Container
- Connector:处理连接部分,可以有多个用来同时提供多个不同协议不同端口的连接
- Container:个service只有一个,用来封装和管理Servlet
- Engine:引擎,用来原理多个站点,一个service只能有一个engine
- host:代表一个站点(虚拟主机),可以添加多个
- context:代表一个应用程序(webapp),一般是对应一个web.xml
- wrapper:每一个Wrapper封装一个servlet
这里要注意的信息是,servlet,filter等都是配置在wepapp的web.xml配置文件中,也就是只对当前webapp生效。因此需求中获取request和response都是针对某一webapp,无法抓取全流量。
一般情况下获取post data
tomcat本身日志不会记录post的数据,一般常用的方法也是添加一个filter,然后配置到web.xml中,由于需求不同,网上的文章中没有提到这个filter只对当前项目生效,这是个坑点,具体代码和用jsp实现相同。
jsp实现
要实现动态注册filter,就需要调用相关API,关键就是要获取到当前webapp的context对象。最底层调用StandardContext.addFilter()就可以添加一个filter。
context,ServletContext,ApplicationContext,StandardContext
context
: 翻译是上下文,用来记录一次请求发生时,web容器中有多少filter,哪些servlet,listener,参数等等ServletContext
:servlet规范的接口,要求context里要有这些字段和方法ApplicationContext
: ServletContext的实现,因为⻔⾯模式的原因,实际套了⼀层ApplicationContextFacade
,实现了接口要求的方法StandardContext
: 比ApplicationContext
更底层实现了ServletContext
接口的类,ApplicationContext
内部都是调用StandardContext
的方法,所以可以理解为是ApplicationContext
是StandardContext
的封装,也是实际起作用的部分
获取webapp的context有很多方法,这几个context的关系如图:
因此获取到任意一个即可,获取方法参考文章:获取context的方法
我们使用jsp实现,可以直接用request获取StandardContext
ServletContext ctx = request.getSession().getServletContext();
Field f = ctx.getClass().getDeclaredField("context");
f.setAccessible(true);
ApplicationContext appCtx = (ApplicationContext)f.get(ctx);
f = appCtx.getClass().getDeclaredField("context");
f.setAccessible(true);
StandardContext standardCtx = (StandardContext)f.get(appCtx);
实现:
抓取response需要实现一个HttpServletResponseWrapper去封装response,然后通过doFilter方法传给下一个过滤器,让后端把response的内容输出到我们封装后的流里,就可以在过滤器中获取到response的内容:
<%
class ResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream buffer;
private ServletOutputStream out;
private MyPrintWriter out2;
public ResponseWrapper(HttpServletResponse response) {
super(response);
buffer = new ByteArrayOutputStream();
out = new WrapperOutputStream(buffer);
out2 = new MyPrintWriter(buffer);
}
// 需要重写两个方法,对应两个class
@Override
public ServletOutputStream getOutputStream() throws IOException {
return out;
}
// 这个是抓取jsp的response部分
@Override
public PrintWriter getWriter() throws IOException {
return out2;
}
@Override
public void flushBuffer() throws IOException {
if (out != null) {
out.flush();
out2.flush();
}
}
public byte[] getContent() throws IOException {
flushBuffer();
return buffer.toByteArray();
}
public byte[] getContent2() throws IOException {
flushBuffer();
return out2.getByteArrayOutputStream().toByteArray();
}
class WrapperOutputStream extends ServletOutputStream {
private ByteArrayOutputStream bos;
public WrapperOutputStream(ByteArrayOutputStream bos) {
this.bos = bos;
}
@Override
public void write(int b) throws IOException {
bos.write(b); // 将数据写到 stream 中
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener arg0) {
}
}
class MyPrintWriter extends PrintWriter {
ByteArrayOutputStream myOutput;
//此即为存放response输入流的对象
public MyPrintWriter(ByteArrayOutputStream output) {
super(output);
myOutput = output;
}
public ByteArrayOutputStream getByteArrayOutputStream() {
return myOutput;
}
}
}
%>
过滤器部分:
<%
ServletContext ctx = request.getSession().getServletContext();
Field f = ctx.getClass().getDeclaredField("context");
f.setAccessible(true);
ApplicationContext appCtx = (ApplicationContext)f.get(ctx);
f = appCtx.getClass().getDeclaredField("context");
f.setAccessible(true);
StandardContext standardCtx = (StandardContext)f.get(appCtx);
f = standardCtx.getClass().getDeclaredField("filterConfigs");
f.setAccessible(true);
Map filterConfigs = (Map)f.get(standardCtx);
if (filterConfigs.get(name) == null) {
out.println("inject "+ name);
Filter filter = new Filter() {
@Override
public void init(FilterConfig arg0) throws ServletException {
// TODO Auto-generated method stub
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterchain)
throws IOException, ServletException {
// TODO Auto-generated method stub
FileWriter fw = new FileWriter("/tmp/logs", true);
Enumeration names = request.getParameterNames();
StringBuilder output = new StringBuilder();
while(names.hasMoreElements()){
String name = (String) names.nextElement();
output.append(name).append("=");
String values[] = request.getParameterValues(name);
for (int i = 0; i < values.length; i++) {
if (i > 0) {
output.append("' ");
}
output.append(values[i]);
}
if (names.hasMoreElements())
output.append("&");
}
fw.write(output + "
");
//fw.write("response.contenttype:" + response.getContentType());
fw.flush();
ResponseWrapper mResp = new ResponseWrapper((HttpServletResponse)response);
// 注意这里一定要放封装之后的对象
filterchain.doFilter(request, mResp);
StringBuilder sb = new StringBuilder();
byte[] bytes = mResp.getContent();
byte[] bytes2 = mResp.getContent2();
sb.append(new String(bytes));
sb.append(new String(bytes2));
System.out.println("length:" + bytes.length+bytes2.length);
System.out.println("String:" + sb);
fw.write(sb.toString());
fw.flush();
fw.close();
response.setContentLength(-1);
if(bytes.length!=0){
response.getOutputStream().write(bytes);
response.getOutputStream().flush();
}else if (bytes2.length!=0){
response.getOutputStream().write(bytes2);
response.getOutputStream().flush();
}
}
@Override
public void destroy() {
// TODO Auto-generated method stub
}
};
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(name);
filterDef.setFilterClass(filter.getClass().getName());
filterDef.setFilter(filter);
standardCtx.addFilterDef(filterDef);
FilterMap m = new FilterMap();
m.setFilterName(filterDef.getFilterName());
m.setDispatcher(DispatcherType.REQUEST.name());
m.addURLPattern("/*");
standardCtx.addFilterMapBefore(m);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
FilterConfig filterConfig = (FilterConfig)constructor.newInstance(standardCtx, filterDef);
filterConfigs.put(name, filterConfig);
out.println("injected");
}
%>
代码中几个坑点:
-
抓取不到response
按照查到的方法,创建一个类ResponseWrapper继承HttpServletResponseWrapper
用来封装response,这样才能在filter中获取到response的内容。封装后第一次无法获取到任何response,查阅文档发现需要把封装后的对象传入doFilter
才会把流输出到我们的封装类的流中。ResponseWrapper mResp = new ResponseWrapper((HttpServletResponse)response); filterchain.doFilter(request, mResp);
-
抓取不到jsp的response
实现后发现只能获取txt,html这种文本类的响应,jsp这种无法获取到。调试之后发现jsp和其他请求调用栈不同,jsp编译成class之后,通过jspservlet调用到后端,response流的输出使用PrintWriter。而其他的都是使用defaultservlet,response流的输出使用ServletOutputStream#write。为了能够获取到全部流量,需要在ResponseWrapper中重新两个方法,一个是getOutputStream()
另一个是getWriter()
。(保险起见可以把父类中所有类似方法都重写一遍),最终处理时找有内容的流读出即可。 -
抓取流之后,返回页面response为空,状态码可能是200或者4xx
流被我们的filter截取后,我们读取并且flush刷新了流,自定义流中就没有了数据,另外实际上返回到浏览器的还是原本的response流,所有需要手动把返回结果再写入到response的=输出流中。response.getOutputStream().write(bytes); response.getOutputStream().flush();
总结
如果是获取某个webapp的所有http流量,可以使用这种方法,jsp文件的方法可以实现动态注入filter,不需要重启服务和修改配置文件,但是jsp文件本身也是一种过时的技术,正常的解决方案肯定还是从开发侧配置。
如果要获取全部的http流量,那需要修改tomcat本身的web.xml,增加全局filter,目前没有发现可以动态注入tomcat全局filter的方法(也就是无法动态获取tomcat的全局context)。
参考链接
https://www.cnblogs.com/tanshaoshenghao/p/10741160.html
https://blog.csdn.net/qq_38245537/article/details/79009448
https://xz.aliyun.com/t/9914#toc-3