在Tomcat自定义阀中包装请求以允许读取请求正文

时间:2017-10-10 15:18:51

标签: java tomcat tomcat-valve

我被要求开发一个Tomcat阀门,记录所有HTTP请求,包括它们的身体。由于包含正文的流只能读取一次,我发现我需要包装请求。我在这里找到了一个基于JBOSS的例子(下载链接" Maven项目的阀门,用于转移与身体的完整请求"):

https://bz.apache.org/bugzilla/show_bug.cgi?id=45014

我将其改编为使用vanilla Tomcat和更新的API(我使用tomcat-catalina:8.5.20)。

这是我的阀门的样子:

public class CaptureValve extends ValveBase {
// ...
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
    // Wrap request so the body can be read multiple times
    RequestWrapper wrappedRequest = new RequestWrapper(request);

    // important - otherwise, requests aren't passed further down the chain...
    getNext().invoke(wrappedRequest, response);

    // Simplified for demo purposes - now I'm reading the body to log it
    LogBody(wrappedRequest.getBody());
}
// ...
}

现在RequestWrapper正如你想象的那样,只代理对GetRequest以外的包装对象的调用:这是该类的相关部分:

public class RequestWrapper extends Request {
//...
    public RequestWrapper(Request wrapped) throws IOException {
        wrappedCatalinaRequest = wrapped;
        loggingRequest = new LoggingRequest(wrapped);
    }    
//...
    @Override
    public HttpServletRequest getRequest() {
        // here is where the actual request used to read from is retrieved
        logger.info("getRequest()");
        return loggingRequest;
    }
//...
}

所以下一部分是LoggingRequest,它包裹内部RequestFacade

private class LoggingRequest extends RequestFacade {

    private LoggingInputStream is;

    LoggingRequest(Request request) throws IOException {
        super(request);

        int len = 0;
        try {
            len = Integer.parseInt(request.getHeader("content-length"));
        } catch (NumberFormatException e) {
            // ignore and assume 0 length
        }

        String contentType = request.getHeader("content-type");

        if (contentType != null) {
            for (String ct : contentType.split(";")) {
                String s = ct.trim();
                if (s.startsWith("charset")) {
                    charset = s.substring(s.indexOf("=") + 1);
                    break;
                }
            }
        }

        // This line causes the issues I describe below
        is = new LoggingInputStream(request.getRequest().getInputStream(), len, charset);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        logger.info("LoggingRequest.getInputStream()");
        return is;
    }

   @Override
    public BufferedReader getReader() throws IOException {
        logger.info("LoggingRequest.getReader()");
        return new BufferedReader(new InputStreamReader(is, charset));
    }

    public String getPayload() {
        logger.info("Method: " + new Object() {}.getClass().getEnclosingMethod().getName());
        return is.getPayload();
    }
}

请注意我将输入流分配给is变量的行。这是我在下面描述的问题开​​始的地方。

最后,ServletInputStream的包装器 - 正如您所看到的,当从实际的Tomcat应用程序读取主体时,想法是将读取的字节也写入缓冲区,然后可以读取再次通过getPayload()方法。我删除了代码的明显部分,你可以在链接的示例项目中找到它,如果你想看到所有细节,我可以从中找到它:

public class LoggingInputStream extends ServletInputStream {
    //...
    public LoggingInputStream(ServletInputStream inputStream, int length, String charset) {
        super();
        is = inputStream;
        bytes = new ByteArrayOutputStream(length);
        charsetName = (charset == null ? "UTF-8" : charset);
    }

    /*
    * Since we are not sure which method will be used just override all 4 of them:
    */
    @Override
    public int read() throws IOException {
        logger.info("LoggingInputStream.read()");
        int ch = is.read();
        if (ch != -1) {
            bytes.write(ch);
            //            logger.info("read:" + ch);
            //            logger.info("bytes.size()=" + bytes.size());
        }
        return ch;
    }

    @Override
    public int read(byte[] b) throws IOException {
        logger.info("LoggingInputStream.read(byte[] b)");
        //        logger.info("byte[].length=" + b.length);
        //        logger.info("byte[]=" + b);
        int numBytesRead = is.read(b);
        if (numBytesRead != -1) {
            for (int i = 0; i < numBytesRead; i++) {
                bytes.write(b[i]);
            }
        }
        return numBytesRead;
    }

    @Override
    public int read(byte[] b, int o, int l) throws IOException {
        logger.info("LoggingInputStream.read(byte[] b, int o, int l)");
        int numBytesRead = is.read(b, o, l);
        if (numBytesRead != -1) {
            for (int i = o; i < numBytesRead; i++) {
                bytes.write(b[i]);
            }
        }
        return numBytesRead;
    }

    @Override
    public int readLine(byte[] b, int o, int l) throws IOException {
        logger.info("LoggingInputStream.readLine(byte[] b, int o, int l)");
        int numBytesRead = is.readLine(b, o, l);
        if (numBytesRead != -1) {
            for (int i = o; i < numBytesRead; i++) {
                bytes.write(b[i]);
            }
        }
        return numBytesRead;
    }

    @Override
    public boolean isFinished() {
        logger.info("isFinished");
        try {
            return is.available() == 0;
        }
        catch (IOException ioe) {
            return false;
        }
    }

    @Override
    public boolean isReady() {
        logger.info("isReady");
        return true;
    }

    @Override
    public void setReadListener(ReadListener listener) {
        throw new RuntimeException("Not implemented");
    }

    public String getPayload() {
        if (bytes.size() > 0) {
            try {
                sb.append(bytes.toString(charsetName));
            } catch (UnsupportedEncodingException e) {
                sb.append("Error occurred when attempting to read request body with charset '").append(charsetName).append("': ");
                sb.append(e.getMessage());
            }
        }

        return sb.toString();
    }
}

到目前为止一切顺利,我实际上已经开始工作了。我写了一个非常简单的Spring应用程序,它有一个基本的POST请求方法,我从Postman调用它来测试它。它很简单:

public String testPost(String body) {
    return body;
}

我发送了一个带有Postman测试请求的尸体,我得到了我从呼叫中发回的身体 - 我的阀门也能够读取身体并记录它。

但是当我想将它与实际的Tomcat应用程序一起使用时,它应该可以使用,它只是不起作用。该应用程序似乎无法再读取请求的正文。我可以在我的日志中看到从未调用流的read()方法。所以我尝试了另一个应用程序 - 因为我只使用了Tomcat Manager应用程序并将Web应用程序的会话到期时间设置为另一个值(这也是一个非常简单的POST请求)。它也不起作用......包含新超时值的主体永远不会到达Tomcat应用程序。但它适用于我自己的Spring应用程序。

还记得上面提到的这条线吗?

is = new LoggingInputStream(request.getRequest().getInputStream(), len, charset);

我追踪这一行作为原因 - 只要我在该行中发表评论,无论我是否注释掉此行之后的任何代码,问题都会出现 - 目标应用程序现在无法读取溪流了。但我只在这里获取请求对象引用并将其分配给另一个变量。我实际上并没有在这里阅读这篇文章。

我有点失落,很高兴任何想法在这里可能出错。

哦,目标tomcat版本是8.0.46而我是9.0和8.5(测试了所有三个,相同的结果)。

编辑:对我的包装器对象进行记录调用

RequestWrapper.<init> ctor RequestWrapper
RequestWrapper$LoggingRequest.<init> ctor LoggingRequest
LoggingInputStream.<init> LoggingInputStream length: 7
RequestWrapper.getContext Method: getContext
RequestWrapper.isAsyncSupported Method: isAsyncSupported
RequestWrapper.isAsync Method: isAsync
RequestWrapper.isAsyncDispatching Method: isAsyncDispatching
RequestWrapper.getRequest getRequest() - POST
RequestWrapper.getRequest getRequest() - POST
RequestWrapper.getUserPrincipal Method: getUserPrincipal
RequestWrapper.getSessionInternal Method: getSessionInternal
RequestWrapper.getWrapper Method: getWrapper
RequestWrapper.getRequestPathMB Method: getRequestPathMB
RequestWrapper.getMethod Method: getMethod
RequestWrapper.getMethod Method: getMethod
RequestWrapper.getUserPrincipal Method: getUserPrincipal
RequestWrapper.getNote Method: getNote
RequestWrapper.getCoyoteRequest Method: getCoyoteRequest
RequestWrapper.getCoyoteRequest Method: getCoyoteRequest
RequestWrapper.setAuthType Method: setAuthType
RequestWrapper.setUserPrincipal Method: setUserPrincipal
RequestWrapper.getSessionInternal Method: getSessionInternal
RequestWrapper.getContext Method: getContext
RequestWrapper.changeSessionId Method: changeSessionId
RequestWrapper.getPrincipal Method: getPrincipal
RequestWrapper.getRequestPathMB Method: getRequestPathMB
RequestWrapper.getWrapper Method: getWrapper
RequestWrapper.isAsyncSupported Method: isAsyncSupported
RequestWrapper.getRequestPathMB Method: getRequestPathMB
RequestWrapper.getDispatcherType Method: getDispatcherType
RequestWrapper.setAttribute Method: setAttribute
RequestWrapper.setAttribute Method: setAttribute
RequestWrapper.getFilterChain Method: getFilterChain
RequestWrapper.getAttribute Method: getAttribute
RequestWrapper.getAttribute Method: getAttribute
RequestWrapper.isAsyncDispatching Method: isAsyncDispatching
RequestWrapper.getRequest getRequest() - POST
RequestWrapper.getAttribute Method: getAttribute
RequestWrapper.isAsync Method: isAsync
RequestWrapper.getRequest getRequest() - POST
RequestWrapper.getBody Method: getBody
RequestWrapper$LoggingRequest.getPayload Method: getPayload
LoggingInputStream.getPayload getPayload size: 0
LoggingInputStream.getPayload getPayload result:

修改:我在https://github.com/codekoenig/RequestLoggerValve

添加了一个示例项目

2 个答案:

答案 0 :(得分:1)

getInputStream外,您还应覆盖getReader。看起来您的目标应用程序更喜欢使用阅读器,这就是您从未调用输入流的原因。

答案 1 :(得分:1)

总结我们在评论中的讨论:

只需调用getInputStream()的{​​{1}}或getReader()方法即可更改该请求的内部状态。首先,它将导致异常调用反之亦然方法(访问流后无法访问读取器,反之亦然)。它还会导致受保护的request方法的不同行为。如果曾经调用过任何一种方法,这将中途中止。

这可以在这里看到:Code of Request-class

在第1175行,我们可以看到阻止调用getReader的逻辑,一旦调用了getStream,在第2752行,如果调用了任何一种方法,你可以看到parseParameters()在中间停止。 / p>

我想,这是你烦恼的原因。