使用jsp添加过滤器获取http流量

遇到一个需求,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的方法,所以可以理解为是ApplicationContextStandardContext的封装,也是实际起作用的部分

获取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 + "\n");
            //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");
}
%>

代码中几个坑点:

  1. 抓取不到response
    按照查到的方法,创建一个类ResponseWrapper继承HttpServletResponseWrapper用来封装response,这样才能在filter中获取到response的内容。封装后第一次无法获取到任何response,查阅文档发现需要把封装后的对象传入doFilter才会把流输出到我们的封装类的流中。

    ResponseWrapper mResp = new ResponseWrapper((HttpServletResponse)response); 
    filterchain.doFilter(request, mResp);
    
  2. 抓取不到jsp的response
    实现后发现只能获取txt,html这种文本类的响应,jsp这种无法获取到。调试之后发现jsp和其他请求调用栈不同,jsp编译成class之后,通过jspservlet调用到后端,response流的输出使用PrintWriter。而其他的都是使用defaultservlet,response流的输出使用ServletOutputStream#write。为了能够获取到全部流量,需要在ResponseWrapper中重新两个方法,一个是getOutputStream()另一个是getWriter()。(保险起见可以把父类中所有类似方法都重写一遍),最终处理时找有内容的流读出即可。

  3. 抓取流之后,返回页面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

posted @ 2021-09-27 17:26  ChanGeZ  阅读(101)  评论(0编辑  收藏  举报