zoukankan      html  css  js  c++  java
  • 使用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 + "
    ");
                //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

  • 相关阅读:
    IIS: 必须输入密码手动设置密码同步后
    IIS操作控制类
    SQL对IP地址进行拆分
    HTTP_REFERER的工作方式[转贴]
    如何知道同服务器上都有哪些网站?
    简单判断临时表是否存在
    .NET 3.5 SP 1发布了
    Log Parser很好很强大的IIS日志分析工具
    遍历Request.ServerVariables
    06复杂查询(多数据库表)
  • 原文地址:https://www.cnblogs.com/chengez/p/jsp_dynamic_add_filter.html
Copyright © 2011-2022 走看看