仅对来自远程端点的响应起作用的 Servlet 过滤器“代理”

Posted

技术标签:

【中文标题】仅对来自远程端点的响应起作用的 Servlet 过滤器“代理”【英文标题】:Servlet filter "proxy" that only acts on response from remote endpoint 【发布时间】:2017-06-02 16:39:12 【问题描述】:

我需要某些 HTTP 请求必须重定向到 Spring Boot Web 应用程序/服务,但在请求端,Spring 应用程序什么也不做,而是充当 HTTP 客户端(另一个服务)和请求的真实目的地。但是当响应返回到 Spring 应用程序(从该目的地)时,我需要 Spring 应用程序能够检查响应并可能在需要时对其采取措施。所以:

    HTTP 客户端向http://someapi.example.com 发出请求 网络魔术将请求路由到我的 Spring 应用程序,例如 http://myproxy.example.com 在请求时,此应用程序/代理不执行任何操作,因此请求在 http://someapi.example.com 上转发 http://someapi.example.com 的服务端点将 HTTP 响应返回给代理 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

有什么想法吗?

【问题讨论】:

Spring AOP Aspect/Pointcut 可以做到这一点(例如:***.com/questions/3310115/…) 感谢@MikeM (+1) - 郑重声明,我不反对 AOP 方法,但 如果它是真的不可能使用过滤器来做到这一点!再次感谢! 我会尝试的另一个想法是编写自定义拦截器并使用其afterActionCompletion() 方法。见:docs.spring.io/spring/docs/4.3.x/javadoc-api/org/… 您是否确定了过滤器单独起作用的事实?即不尝试访问来自目的地的响应。 是的,我可以验证我是否正确设置了过滤器,并且如果我在chain.doFilter(...) 之前添加打印/日志语句,它们会按预期执行。 【参考方案1】:

根据 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>");
    

测试网址:

https://yourHost:8181/contextPath/ResponseFilterTest/Fizz(触发响应替换) https://yourHost:8181/contextPath/ResponseFilterTest/(回复不变)

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

【讨论】:

以上是关于仅对来自远程端点的响应起作用的 Servlet 过滤器“代理”的主要内容,如果未能解决你的问题,请参考以下文章

来自本地文件的远程 css 不起作用?

为啥我的 POST 请求仅对我的 API 不起作用?

Testcafe到端点的多个请求同步不起作用

如何使用来自远程服务器的 net.tcp 端点 ping 或检查 WCF 服务的状态?

当我在 Angularjs 中绑定来自响应的数据时,引导多选不起作用

与 Servlet Filter 和 FilterRegistrationBean 一起使用时,跨域资源共享不起作用