远程终端响应专用的Servlet过滤器“代理”

10
我有一个需求,需要将某些HTTP请求重定向到Spring Boot Web应用/服务,但在请求方面,Spring应用程序不会执行任何操作,而是充当HTTP客户端(另一个服务)和请求的真实目标之间的中转。但是当响应从该目标返回到Spring应用程序时,如果需要,我需要Spring应用程序能够检查响应并可能采取行动。因此:
  1. HTTP客户端向http://someapi.example.com发出请求
  2. 网络魔法将请求路由到我的Spring应用程序,例如http://myproxy.example.com
  3. 在请求上,此应用程序/代理什么也不做,因此请求被转发到http://someapi.example.com
  4. http://someapi.example.com服务端点将HTTP响应返回到代理
  5. 代理在http://myproxy.example.com检查此响应,并在将响应返回给原始客户端之前可能发送警报。
因此,本质上是一个过滤器,在请求方面充当一个通道,在远程服务已经执行并返回响应之后才真正起作用。
我迄今为止尝试的最佳方法是设置一个Servlet过滤器:
@Override
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    chain.doFilter(request, response)

    // How and where do I put my code?
    if(responseContainsFizz(response)) {
        // Send an alert (don't worry about this code)
    }
}

这个有可能实现吗?如果可以,检查和处理响应的代码应该放在哪里?我的代码出现异常,当我尝试从浏览器访问控制器时:

java.lang.IllegalStateException: STREAM
    at org.eclipse.jetty.server.Response.getWriter(Response.java:910) ~[jetty-server-9.2.16.v20160414.jar:9.2.16.v20160414]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_92]
  rest of stack trace omitted for brevity

有什么想法吗?

1
Spring AOP的Aspect/Pointcut可以实现这个功能(例如:https://dev59.com/kHA75IYBdhLWcg3waIQJ) - MikeM
谢谢 @MikeM (+1) - 为了记录,我并不反对使用AOP方法,但只有在真的无法通过过滤器来实现时才会考虑!再次感谢! - smeeb
我会尝试的另一个想法是编写自定义拦截器并使用其 afterActionCompletion() 方法。请参阅:http://docs.spring.io/spring/docs/4.3.x/javadoc-api/org/springframework/web/portlet/HandlerInterceptor.html#afterActionCompletion-javax.portlet.ActionRequest-javax.portlet.ActionResponse-java.lang.Object-java.lang.Exception- - Slava Semushin
那么可能是响应包含Fizz的某些内容导致了这个问题。我们能看到该方法的代码吗? - gipsy
哦!你的意思是如果你放置了chain.doFilter(request, response)它会失败? - gipsy
显示剩余4条评论
2个回答

5
根据Servlet API文档,您之所以收到“IllegalStateException”是因为您尝试在响应上调用“ServletResponse.getOutputStream”后调用“ServletResponse.getWriter”。因此,看起来您需要调用的方法是“ServletResponse.getOutputStream()”。
但是,如果您想访问响应正文,则最好的解决方案是将响应包装在“ServletResponseWrapper”中,以便您可以捕获数据:
public class MyFilter implements Filter
{
    @Override
    public void init(FilterConfig filterConfig) throws ServletException
    {

    }

    @Override
    public void destroy()
    {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
    {
        MyServletResponseWrapper responseWrapper = new MyServletResponseWrapper((HttpServletResponse) response);
        chain.doFilter(request, responseWrapper);
        if (evaluateResponse(responseWrapper)) {
            // Send an alert
        }
    }

    private boolean evaluateResponse(MyServletResponseWrapper responseWrapper) throws IOException
    {
        String body = responseWrapper.getResponseBodyAsText();

        // Perform business logic on the body text

        return true;
    }

    private static class MyServletResponseWrapper extends HttpServletResponseWrapper
    {
        private ByteArrayOutputStream copyOutputStream;
        private ServletOutputStream wrappedOutputStream;

        public MyServletResponseWrapper(HttpServletResponse response)
        {
            super(response);
        }

        public String getResponseBodyAsText() throws IOException
        {
            String encoding = getResponse().getCharacterEncoding();
            return copyOutputStream.toString(encoding);
        }


        @Override
        public ServletOutputStream getOutputStream() throws IOException
        {
            if (wrappedOutputStream == null) {
                wrappedOutputStream = getResponse().getOutputStream();
                copyOutputStream = new ByteArrayOutputStream();
            }
            return new ServletOutputStream()
            {
                @Override
                public boolean isReady()
                {
                    return wrappedOutputStream.isReady();
                }

                @Override
                public void setWriteListener(WriteListener listener)
                {
                    wrappedOutputStream.setWriteListener(listener);
                }

                @Override
                public void write(int b) throws IOException
                {
                    wrappedOutputStream.write(b);
                    copyOutputStream.write(b);
                }

                @Override
                public void close() throws IOException
                {
                    wrappedOutputStream.close();
                    copyOutputStream.close();
                }
            };
        }
    }
}

2

通过使用过滤器和响应包装器,可以轻松地操纵/替换/扩展响应。

在调用chain.doFilter(request, wrapper)之前,在过滤器中准备一个新的响应内容的PrintWriter和包装器对象。

在调用chain.doFilter(request, wrapper)之后,进行实际的响应操作。

包装器被用于访问响应字符串。

过滤器:

@WebFilter(filterName = "ResponseAnalysisFilter", urlPatterns = { "/ResponseFilterTest/*" })
public class ResponseFilter implements Filter {
    public ResponseFilter() {}

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

    @Override
    public void destroy() {}

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        PrintWriter out = response.getWriter();
        CharResponseWrapper wrapper = new CharResponseWrapper((HttpServletResponse) response);
        chain.doFilter(request, wrapper);

        String oldResponseString = wrapper.toString();

        if (oldResponseString.contains("Fizz")) { 
            // replace something
            String newResponseString = oldResponseString.replaceAll("Fizz", "Cheers");
            // show alert with a javascript appended in the head tag
            newResponseString = newResponseString.replace("</head>", 
               "<script>alert('Found Fizz, replaced with Cheers');</script></head>");

            out.write(newResponseString);
            response.setContentLength(newResponseString.length());
        } 
        else { //not changed
            out.write(oldResponseString);
        }
        // the above if-else block could be replaced with the code you need.
        // for example: sending notification, writing log, etc.

        out.close();
    }
}

响应包装器:

public class CharResponseWrapper extends HttpServletResponseWrapper {
    private CharArrayWriter output;

    public String toString() {
        return output.toString();
    }

    public CharResponseWrapper(HttpServletResponse response) {
        super(response);
        output = new CharArrayWriter();
    }

    public PrintWriter getWriter() {
        return new PrintWriter(output);
    }
}

测试Servlet:

@WebServlet("/ResponseFilterTest/*")
public class ResponseFilterTest extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");

        response.getWriter().append(
           "<html><head><title>replaceResponse filter</title></head><body>");

        if (request.getRequestURI().contains("Fizz")) {
            response.getWriter().append("Fizz");
        }
        else {
            response.getWriter().append("Limo");
        }

        response.getWriter().append("</body></html>");
    }
}

测试网址:

有关过滤器的更多信息和示例:
http://www.oracle.com/technetwork/java/filters-137243.html#72674
http://www.leveluplunch.com/java/tutorials/034-modify-html-response-using-filter/
https://punekaramit.wordpress.com/2010/03/16/intercepting-http-response-using-servlet-filter/


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接