验证调用HttpServletResponse.getWriter().close()方法是否真的会关闭http连接

亦凉 2024-04-03 09:44 94阅读 0赞

起因

线上项目突然遭到大量的非法参数攻击,由于历史问题,之前的代码从未对请求参数进行校验。
导致大量请求落到了数据访问层,给应用服务器和数据库都带来了很大压力。
针对这个问题,只能对请求真正到Controller方法调用之前直接将非法参数请求拒绝掉,所以在Filter中对参数进行统一校验,非法参数直接返回400。
我的建议是不但要设置响应状态码设置为400,还应该明确调用HttpServletResponse.getWriter().close(),希望此举能在服务端主动断开连接,释放资源。
但是同事认为不必要明确调用HttpServletResponse.getWriter().close(),于是就有了这个验证实验。

实验

1.应用容器:tomcat 7.0.59

2.如何验证服务器是否真的断开连接:观察http响应消息头“Connection”值是否为“close”。

不明确close时httpresponse返回的消息头

  1. HTTP/1.1 400 Bad Request
  2. Server: Apache-Coyote/1.1
  3. Content-Length: 21
  4. Date: Tue, 05 Sep 2017 11:39:00 GMT
  5. Connection: close

明确close时httpresponse返回的消息头

  1. HTTP/1.1 400 Bad Request
  2. Server: Apache-Coyote/1.1
  3. Content-Length: 0
  4. Date: Tue, 05 Sep 2017 11:39:25 GMT
  5. Connection: close

结论

1.根据上述结果,如果根据http响应消息头“Connection”值是否为“close”来验证服务端是否会主动断开连接。
那么在servlet中是否明确调用“HttpServletResponse.getWriter().close()”结果都是一样的。
因为在org.apache.coyote.http11.AbstractHttp11Processor中会根据响应状态码判断返回消息头Connection值。

  1. private void prepareResponse() {
  2. ...
  3. // If we know that the request is bad this early, add the
  4. // Connection: close header.
  5. keepAlive = keepAlive && !statusDropsConnection(statusCode);
  6. if (!keepAlive) {
  7. // Avoid adding the close header twice
  8. if (!connectionClosePresent) {
  9. headers.addValue(Constants.CONNECTION).setString(
  10. Constants.CLOSE);
  11. }
  12. } else if (!http11 && !getErrorState().isError()) {
  13. headers.addValue(Constants.CONNECTION).setString(Constants.KEEPALIVE);
  14. }
  15. ...
  16. }
  17. /**
  18. * Determine if we must drop the connection because of the HTTP status
  19. * code. Use the same list of codes as Apache/httpd.
  20. */
  21. protected boolean statusDropsConnection(int status) {
  22. return status == 400 /* SC_BAD_REQUEST */ ||
  23. status == 408 /* SC_REQUEST_TIMEOUT */ ||
  24. status == 411 /* SC_LENGTH_REQUIRED */ ||
  25. status == 413 /* SC_REQUEST_ENTITY_TOO_LARGE */ ||
  26. status == 414 /* SC_REQUEST_URI_TOO_LONG */ ||
  27. status == 500 /* SC_INTERNAL_SERVER_ERROR */ ||
  28. status == 503 /* SC_SERVICE_UNAVAILABLE */ ||
  29. status == 501 /* SC_NOT_IMPLEMENTED */;
  30. }

也就是说,当响应状态码为400时,不论是否明确调用“HttpServletResponse.getWriter().close()”,都会在响应消息头中设置“Connection: close”。
那么,问题来了:HTTP的响应消息头“Connection”值为“close”时是否就意味着服务端会主动断开连接了呢?
根据rfc2616的对于HTTP协议的定义(详见:https://www.ietf.org/rfc/rfc2616.txt):

  1. HTTP/1.1 defines the "close" connection option for the sender to
  2. signal that the connection will be closed after completion of the
  3. esponse. For example,
  4. Connection: close

也就是说,一旦在服务端设置响应消息头“Connection”为“close”,就意味着在本次请求响应完成后,对应的连接应该会被关闭。
然而,这对于不同的Servlet容器实现来说,真的就会关闭连接吗?
跟踪tomcat源码发现,即使明确调用close()方法也不是直接就关闭连接。

2.明确调用“HttpServletResponse.getWriter().close()”时tomcat又做了什么事情

  1. (1)org.apache.catalina.connector.CoyoteWriter
  2. @Override
  3. public void close() {
  4. // We don't close the PrintWriter - super() is not called,
  5. // so the stream can be reused. We close ob.
  6. try {
  7. ob.close();
  8. } catch (IOException ex ) {
  9. // Ignore
  10. }
  11. error = false;
  12. }
  13. (2)org.apache.catalina.connector.OutputBuffer
  14. /**
  15. * Close the output buffer. This tries to calculate the response size if
  16. * the response has not been committed yet.
  17. *
  18. * @throws IOException An underlying IOException occurred
  19. */
  20. @Override
  21. public void close()
  22. throws IOException {
  23. if (closed) {
  24. return;
  25. }
  26. if (suspended) {
  27. return;
  28. }
  29. // If there are chars, flush all of them to the byte buffer now as bytes are used to
  30. // calculate the content-length (if everything fits into the byte buffer, of course).
  31. if (cb.getLength() > 0) {
  32. cb.flushBuffer();
  33. }
  34. if ((!coyoteResponse.isCommitted()) && (coyoteResponse.getContentLengthLong() == -1) &&
  35. !coyoteResponse.getRequest().method().equals("HEAD")) {
  36. // If this didn't cause a commit of the response, the final content
  37. // length can be calculated. Only do this if this is not a HEAD
  38. // request since in that case no body should have been written and
  39. // setting a value of zero here will result in an explicit content
  40. // length of zero being set on the response.
  41. if (!coyoteResponse.isCommitted()) {
  42. coyoteResponse.setContentLength(bb.getLength());
  43. }
  44. }
  45. if (coyoteResponse.getStatus() ==
  46. HttpServletResponse.SC_SWITCHING_PROTOCOLS) {
  47. doFlush(true);
  48. } else {
  49. doFlush(false);
  50. }
  51. closed = true;
  52. // The request should have been completely read by the time the response
  53. // is closed. Further reads of the input a) are pointless and b) really
  54. // confuse AJP (bug 50189) so close the input buffer to prevent them.
  55. Request req = (Request) coyoteResponse.getRequest().getNote(
  56. CoyoteAdapter.ADAPTER_NOTES);
  57. req.inputBuffer.close();
  58. coyoteResponse.finish();
  59. }
  60. (3)org.apache.coyote.Response
  61. public void finish() {
  62. action(ActionCode.CLOSE, this);
  63. }
  64. public void action(ActionCode actionCode, Object param) {
  65. if (hook != null) {
  66. if( param==null )
  67. hook.action(actionCode, this);
  68. else
  69. hook.action(actionCode, param);
  70. }
  71. }
  72. (4)org.apache.coyote.http11.AbstractHttp11Processor
  73. /**
  74. * Send an action to the connector.
  75. *
  76. * @param actionCode Type of the action
  77. * @param param Action parameter
  78. */
  79. @Override
  80. @SuppressWarnings("deprecation") // Inbound/Outbound based upgrade mechanism
  81. public final void action(ActionCode actionCode, Object param) {
  82. switch (actionCode) {
  83. case CLOSE: {
  84. // End the processing of the current request
  85. try {
  86. getOutputBuffer().endRequest();
  87. } catch (IOException e) {
  88. setErrorState(ErrorState.CLOSE_NOW, e);
  89. }
  90. break;
  91. }
  92. ...
  93. }
  94. }
  95. (5)org.apache.coyote.http11.InternalNioOutputBuffer
  96. /**
  97. * End request.
  98. *
  99. * @throws IOException an underlying I/O error occurred
  100. */
  101. @Override
  102. public void endRequest() throws IOException {
  103. super.endRequest();
  104. flushBuffer();
  105. }
  106. /**
  107. * Callback to write data from the buffer.
  108. */
  109. private void flushBuffer() throws IOException {
  110. //prevent timeout for async,
  111. SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector());
  112. if (key != null) {
  113. NioEndpoint.KeyAttachment attach = (NioEndpoint.KeyAttachment) key.attachment();
  114. attach.access();
  115. }
  116. //write to the socket, if there is anything to write
  117. if (socket.getBufHandler().getWriteBuffer().position() > 0) {
  118. socket.getBufHandler().getWriteBuffer().flip();
  119. writeToSocket(socket.getBufHandler().getWriteBuffer(),true, false);
  120. }
  121. }

实际上,明确调用“HttpServletResponse.getWriter().close()”时只是确保将数据发送给客户端,并不会执行关闭连接。
因此,回到我一开始的疑问:是否需要在代码中明确调用close()方法?在我遇到的这个校验非法参数的场景,其实是不必要的。但是,当HTTP状态码返回400时,Connection值一定会被设置为close。
那么,这个问题被引申一下:Http协议头中的“Connection”字段到底有和意义呢?这需要从HTTP协议说起。在Http1.0中是没有这个字段的,也就是说每一次HTTP请求都会建立新的TCP连接。而随着Web应用的发展,通过HTTP协议请求的资源越来越丰富,除了文本还可能存在图片等其他资源了,为了能够在一次TCP连接中能最快地获取到这些资源,在HTTP1.1中增加了“Connection”字段,取值为close或keep-alive。其作用在于告诉使用HTTP协议通信的2端在建立TCP连接并完成第一次HTTP数据响应之后不要直接断开对应的TCP连接,而是维持这个TCP连接,继续在这个连接上传输后续的HTTP数据,这样可以大大提高通信效率。当然,当“Connection”字段值为close时,说明双方不再需要通信了,希望断开TCP连接。
所以,对于使用HTTP协议的Web应用来讲,如果希望服务器端与客户端在本次HTTP协议通信之后断开连接,需要将“Connection”值设置为close;否则应该设置为keep-alive。

3.针对非法参数的DDoS攻击的请求,都应该在应用服务器前端进行拦截,杜绝请求直接到应用层。

发表评论

表情:
评论列表 (有 0 条评论,94人围观)

还没有评论,来说两句吧...

相关阅读