6

我需要一个 servlet 过滤器来捕获所有输入,然后破坏该输入,在每种形式中插入一个特殊标记。想象一下,过滤器与所有请求(例如url-pattern=*)相关联。我有用于捕获内容的代码,但它似乎RequestWrapper不够强大,无法捕获所有输入。某些输入返回零字节,然后我无法将该内容“流式传输”回给用户。例如,我们仍在使用 Struts 1.3.10 并且任何 Struts 代码都不能正确“捕获”,我们得到零字节内容。我相信这是因为 Struts 处理前锋的方式。如果请求中涉及到转发,我想知道下面的捕获代码是否可以工作。这是所有代码,您是否有一种方法可以捕获旨在流式传输给用户的任何类型的内容。

<filter>
   <filter-name>Filter</filter-name>
   <filter-class>mybrokenCaptureHtml.TokenFilter</filter-class>
</filter> 
<filter-mapping>
    <filter-name>Filter</filter-name>
    <url-pattern>/*</url-pattern>
 </filter-mapping>

package mybrokenCaptureHtml;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

public class TokenFilter implements Filter {    
    @Override
    public void destroy() {
    }

    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;               
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        try {                                                                                       
            final MyResponseWrapper responseWrapper = new MyResponseWrapper((HttpServletResponse) response);
            chain.doFilter(request, responseWrapper);                       

            // **HERE DEPENDING ON THE SERVLET OR APPLICATION CODE (STRUTS, WICKET), the response returns an empty string //
            // Especiall struts, is there something in their forwards that would cause an error?
            final byte [] bytes = responseWrapper.toByteArray(); 
                    // For some applications that hit this filter
                    // ZERO BYTE DATA is returned, this is bad, but SOME
                    // CODE, the data is captured.
            final String origHtml = new String(bytes);

            final String newHtml = origHtml.replaceAll("(?i)</(\\s)*form(\\s)*>", "<input type=\"hidden\" name=\"zval\" value=\"fromSiteZ123\"/></form>");          
            response.getOutputStream().write(newHtml.getBytes());

        } catch(final Exception e) {            
            e.printStackTrace();
        }
        return;
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {        
    }

    static class MyResponseWrapper extends HttpServletResponseWrapper {    
        private final MyPrintWriter pw = new MyPrintWriter();               
        public byte [] toByteArray() {            
            return pw.toByteArray();        
        }
        public MyResponseWrapper(HttpServletResponse response) {
            super(response);       
        }

        @Override
        public PrintWriter getWriter() {
            return pw.getWriter();
        }
        @Override
        public ServletOutputStream getOutputStream() {
            return pw.getStream();
        }       
        private static class MyPrintWriter {
            private ByteArrayOutputStream baos = new ByteArrayOutputStream();
            private PrintWriter pw = new PrintWriter(baos);
            private ServletOutputStream sos = new MyServletStream(baos);
            public PrintWriter getWriter() {
                return pw;
            }
            public ServletOutputStream getStream() {
                return sos;
            }
            byte[] toByteArray() {
                return baos.toByteArray();
            }
        }    
        private static class MyServletStream extends ServletOutputStream {
            ByteArrayOutputStream baos;
            MyServletStream(final ByteArrayOutputStream baos) {
                this.baos = baos;
            }
            @Override
            public void write(final int param) throws IOException {
                baos.write(param);
            }
        }
    }

}

这是一个示例 Struts 应用程序可能看起来的样子,对于某些应用程序(不是 Struts),我们可能会捕获内容。但是对于像下面这样的应用程序,HTML 内容返回零字节,但应该有内容。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>
<%@ taglib uri="/WEB-INF/struts-tiles.tld" prefix="tiles" %>
<%@ taglib uri="/WEB-INF/struts-nested.tld" prefix="nested"%>
<html:html>
<head>
<title><bean:message key="myApp.customization.title" /></title>
<LINK rel="stylesheet" type="text/css" href="../theme/styles.css">
</head>
<body>
<html:form styleId="customizemyAppForm" method="post" action="/customizemyApp.do?step=submit">
<html:submit onclick="javascript:finish(this.form);" styleClass="input_small">&nbsp;&nbsp;<bean:message key="myApp.customization.submit" />&nbsp;</html:submit> 
<input type="button" styleClass="input_small" width="80" style="WIDTH:80px" name="<bean:message key="myApp.customization.cancel" />" value="<bean:message key="myApp.customization.cancel" />" onclick="javascript:cancel();">

</html:form>
</body>
</html:html>

我怀疑MyResponseWrapperMyPrintWriter不够健壮,无法捕获所有类型的内容。


可以工作的示例 servlet(a):

response.getOutputStream().write(str.getBytes());

不起作用的示例 servlet ( b ):

response.getWriter().println("<html>data</html>");

示例a将获得捕获,示例b不会。

这是一个改进的包装类,大多数应用程序都可以工作,但现在一些 struts 应用程序,只有一些响应被发送到浏览器。

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

public class ByteArrayResponseWrapper extends HttpServletResponseWrapper {
    private PrintWriter output = null;
    private ServletOutputStream outStream = null;
    private static final String NL = System.getProperty("line.separator");

    public ByteArrayResponseWrapper(final HttpServletResponse response) {
        super(response);
    }

    public String getDocument() {        
        InputStream in = null;
        try {            
            in = this.getInputStream();            
            if (in != null) {             
                return getDocument(in);
            }           
        } catch(final Exception ee) {
            // ee.print;StackTrace();
        } finally {                  
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    //e.prin;tStackTrace();
                }
            }
        }
        return "";    
    }

    protected String getDocument(final InputStream in) {
        final StringBuffer buf = new StringBuffer();
        BufferedReader br = null;
        try {
            String line = "";
            br = new BufferedReader(new InputStreamReader(getInputStream(), this.getCharacterEncoding()));            
            while ((line = br.readLine()) != null) {
                buf.append(line).append(NL);                
            }
        } catch(final IOException e) {
            //e.print;StackTrace();
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
            } catch (IOException ex) {             
            }
        }
        return buf.toString();
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        if (output == null) {
            output = new PrintWriter(new OutputStreamWriter(getOutputStream(), this.getCharacterEncoding()));
        }
        return output;
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        if (outStream == null) {
            outStream = new BufferingServletOutputStream();
        }
        return outStream;
    }

    public InputStream getInputStream() throws IOException {
        final BufferingServletOutputStream out = (BufferingServletOutputStream) getOutputStream();        
        return new ByteArrayInputStream(out.getBuffer().toByteArray());
    }

    /**
     * Implementation of ServletOutputStream that handles the in-memory
     * buffering of the response content
     */
    public static class BufferingServletOutputStream extends ServletOutputStream {
        ByteArrayOutputStream out = null;

        public BufferingServletOutputStream() {
            this.out = new ByteArrayOutputStream();
        }

        public ByteArrayOutputStream getBuffer() {
            return out;
        }

        public void write(int b) throws IOException {
            out.write(b);
        }

        public void write(byte[] b) throws IOException {
            out.write(b);
        }

        public void write(byte[] b, int off, int len) throws IOException {
            out.write(b, off, len);
        }
        @Override
        public void close() throws IOException {
            out.close();
            super.close();
        }
        @Override
        public void flush() throws IOException {
            out.flush();
            super.flush();
        }
    }
}

我找到了一个可能的解决方案,在该getInputStream方法中,如果我在所有对象上调用“关闭”,例如outStream.flush()然后outStream.close()然后out.flush()...out.close()看起来最终字节被正确写入。它不直观,但看起来很有效。

4

1 回答 1

7

您最初的方法失败了,因为用具有8192 个字符的内部字符缓冲区的a包装给定,并且在从. 换句话说,当写入响应的数据少于 8KB 时,包装实际上永远不会被填充;即所有内容仍在该内部字符缓冲区中,等待刷新。PrintWriter ByteArrayOutputStreamBufferedWriterflush()ByteArrayOutputStreamgetWriter()ByteArrayOutputStream

解决方法是在您之前执行flush()呼叫:toByteArray()MyPrintWriter

byte[] toByteArray() {
    pw.flush();
    return baos.toByteArray();
}

这样内部字符缓冲区将被刷新(即它实际上会将所有内容写入包装的流)。这也完全解释了为什么它在您写入时起作用getOutputStream(),这一步即不使用PrintWriter并且在某些内部缓冲区中没有任何内容被缓冲。


与具体问题无关:这种方法有一些严重的问题。它在构造过程中不尊重响应字符编码PrintWriter(您实际上应该将其包装ByteArrayOutputStream在一个OutputStreamWriter可以接受字符编码的替代中)并依赖于平台默认值,换句话说,任何书面的 Unicode 字符都可能以这种方式在Mojibake中结束因此,这种方法还没有准备好统治世界。

此外,这种方法可以同时调用getWriter()相同getOutputStream()的响应,而这被认为是非法状态(正是为了避免这种缓冲和编码问题)。


根据评论进行更新,这是对响应包装器的完全重写,显示了正确的方式,希望以比您迄今为止的代码更自我解释的方式:

public class CapturingResponseWrapper extends HttpServletResponseWrapper {

    private final ByteArrayOutputStream capture;
    private ServletOutputStream output;
    private PrintWriter writer;

    public CapturingResponseWrapper(HttpServletResponse response) {
        super(response);
        capture = new ByteArrayOutputStream(response.getBufferSize());
    }

    @Override
    public ServletOutputStream getOutputStream() {
        if (writer != null) {
            throw new IllegalStateException("getWriter() has already been called on this response.");
        }

        if (output == null) {
            output = new ServletOutputStream() {
                @Override
                public void write(int b) throws IOException {
                    capture.write(b);
                }
                @Override
                public void flush() throws IOException {
                    capture.flush();
                }
                @Override
                public void close() throws IOException {
                    capture.close();
                }
            };
        }

        return output;
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        if (output != null) {
            throw new IllegalStateException("getOutputStream() has already been called on this response.");
        }

        if (writer == null) {
            writer = new PrintWriter(new OutputStreamWriter(capture, getCharacterEncoding()));
        }

        return writer;
    }

    @Override
    public void flushBuffer() throws IOException {
        super.flushBuffer();

        if (writer != null) {
            writer.flush();
        }
        else if (output != null) {
            output.flush();
        }
    }

    public byte[] getCaptureAsBytes() throws IOException {
        if (writer != null) {
            writer.close();
        }
        else if (output != null) {
            output.close();
        }

        return capture.toByteArray();
    }

    public String getCaptureAsString() throws IOException {
        return new String(getCaptureAsBytes(), getCharacterEncoding());
    }

}

以下是您应该如何使用它:

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    CapturingResponseWrapper capturingResponseWrapper = new CapturingResponseWrapper((HttpServletResponse) response);
    chain.doFilter(request, capturingResponseWrapper);
    String content = capturingResponseWrapper.getCaptureAsString(); // This uses response character encoding.
    String replacedContent = content.replaceAll("(?i)</form(\\s)*>", "<input type=\"hidden\" name=\"zval\" value=\"fromSiteZ123\"/></form>");
    response.getWriter().write(replacedContent); // Don't ever use String#getBytes() without specifying character encoding!
}
于 2014-04-30T06:36:11.787 回答