目录
为了保证所有接口访问记录可控可查,目前需要将所有核心系统的接口、后门jsp等做访问记录日志打印,其中要包括登陆人、时间、请求参数、接口URI等,实现方式有很多,可以使用过滤器、拦截器、aop切面、log4j框架等,由于要登录人信息,因此这个过滤器还需要实现登陆认证功能。还有一个问题在于需要在日志中打印出请求参数,如果是post请求,请求体通过流读取只能被读取一次,如果在过滤器读取请求体后,后续读取的结果为空,因此也需要特殊处理。本文给大家介绍一下我们使用的 Spring 中一种特殊类型的 Filter(过滤器)OncePerRequestFilter。
OncePerRequestFilter 是什么?
Filter 可以在 Servlet 执行之前或之后调用。当请求被调度给一个 Servlet 时,RequestDispatcher 可能会将其转发给另一个 Servlet。另一个 Servlet 也有可能使用相同的 Filter。在这种情况下,同一个 Filter 会被调用多次。
一个请求会经过两个 Servlet 处理的典型场景是请求的转发(Forwarding)或者包含(Include)。
-
请求转发(Forwarding): 当一个 Servlet 接收到一个请求后,它可以将请求转发给另一个 Servlet 处理。这种情况下,请求会经过第一个 Servlet,然后被转发给第二个 Servlet。在第二个 Servlet 处理完毕后,响应会返回给第一个 Servlet,最终返回给客户端。
-
请求包含(Include): 当一个 Servlet 接收到一个请求后,它可以包含(Include)另一个 Servlet 处理。这种情况下,请求也会经过第一个 Servlet,然后被包含到第二个 Servlet 中进行处理。处理完成后,响应会返回给第一个 Servlet,最终一起返回给客户端。
以下给一个FIlter可能被调用多次的场景:
// Filter1.java
public class Filter1 implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("Filter1 - before Servlet1");
chain.doFilter(request, response); // 继续调用过滤器链中的下一个过滤器或者目标 Servlet
System.out.println("Filter1 - after Servlet1");
}
}
// Filter2.java
public class Filter2 implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("Filter2 - before Servlet1");
chain.doFilter(request, response); // 继续调用过滤器链中的下一个过滤器或者目标 Servlet
System.out.println("Filter2 - after Servlet1");
}
}
// Servlet1.java
public class Servlet1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("Servlet1 - processing request");
RequestDispatcher dispatcher = request.getRequestDispatcher("/servlet2");
dispatcher.forward(request, response); // 转发请求给 Servlet2
}
}
// Servlet2.java
public class Servlet2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("Servlet2 - processing request");
}
}
注意:规范中,请求流被读取一次后流就会关闭,因此如果在过滤器中读取了流数据,在Servlet中再去读取流数据则会读不到请求体中的数据,针对这个问题,也有比较简单的方法,接下来会介绍。
但是,有时需要确保每个请求只调用一次特定的 Filter。一个常见的用例是在使用 Spring Security 时。当请求通过过滤器链(Filter Chain)时,对请求的身份证认证应该只执行一次。OncePerRequestFilter是Spring框架Spring框架提供的一个过滤器,确保在一次HTTP请求期间只执行一次特定的过滤器逻辑。它继承了GenericFilterBean类,并实现了javax.servlet.Filter接口。在这种情况下,就可以继承 OncePerRequestFilter。
重点方法
在 OncePerRequestFilter 中,有一个名为 shouldNotFilter 的方法,它用于指定哪些请求不应该被当前过滤器处理。这个方法是一个模板方法,由子类重写以提供自定义的逻辑。如果 shouldNotFilter 方法返回 true,则当前请求不会被当前过滤器处理,而是直接继续执行过滤器链中的下一个过滤器或目标资源。如果 shouldNotFilter 方法返回 false,则当前过滤器将处理当前请求。 shouldNotFilter 方法的默认实现始终返回 false,这意味着默认情况下当前过滤器会处理所有的请求。如果需要特定的过滤逻辑,可以重写 shouldNotFilter 方法来实现自定义的逻辑。
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
return false;
}
doFilterInternal 是 OncePerRequestFilter 抽象类中的一个抽象方法,用于执行实际的过滤逻辑。所有继承了 OncePerRequestFilter 的子类都必须实现这个方法来定义自己的过滤逻辑。
protected abstract void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
doFilterInternal 方法接收三个参数:
HttpServletRequest request:表示客户端发来的 HTTP 请求。通过这个对象可以获取请求的信息,如请求头、请求参数等。
HttpServletResponse response:表示将要发送给客户端的 HTTP 响应。通过这个对象可以设置响应的状态码、响应头、响应体等。
FilterChain filterChain:表示过滤器链。在 doFilterInternal 方法中,通过调用 filterChain.doFilter(request, response) 可以将请求传递给过滤器链中的下一个过滤器或目标资源。 在实现时,可以选择调用 filterChain.doFilter(request, response)
来将请求传递给过滤器链中的下一个过滤器或目标资源,也可以选择直接处理请求,不再将请求传递下去。
怎么使用?
首先是根据需要重写shouldNotFilter,自定义过滤执行的规则
public class AccessorLogFilter extends OncePerRequestFilter {
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
//定义过滤规则(URI里包含/healthcheck.html的请求不执行该过滤器)
if (request.getRequestURI().contains("/healthcheck.html") ) {
return true;
}
//否则,默认返回false
return super.shouldNotFilter(request);
}
}
接着重写doFilterInternal,执行过滤逻辑
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 按照规范打印需要的日志
log.warn();
// 根据需要选择是否需要传递到下一个过滤组件
filterChain.doFilter(req, resp);
}
怎么实现请求流重复读?
前面提到一个问题:请求流被读取一次后流就会关闭,因此如果在过滤器中读取了流数据,在Servlet中再去读取流数据则会读不到请求体中的数据。本次需求中,由于需要打印请求体中的请求参数,如果在过滤器阶段使用了getReader() 和 getInputStream() 用于获取请求体的内容,那么到了处理器需要读取请求体的时候会导致读取到的内容为空。这对于某些需要多次读取请求体的场景来说可能会造成不便,比如需要进行多次鉴权或日志记录等操作。 针对这个问题,spring提供了一个比较简单的解决方法。
ContentCachingRequestWrapper 是 Spring Framework 提供的一个包装器(Wrapper),用于包装 Servlet 容器的请求对象(HttpServletRequest)。它的作用是在请求被处理前,对请求的内容进行缓存,使得请求的内容可以被多次读取。它会在第一次读取请求体时,将请求体内容缓存起来。然后,可以通过 getContentAsByteArray()、getContentAsByteArray()、getReader() 或 getInputStream() 等方法来多次读取请求体内容。
同样的,ContentCachingResponseWrapper 也是Spring Framework 提供的一个包装器(Wrapper),用于在 Java Spring 框架中进行 HTTP 响应的缓存处理。它允许开发人员在对响应进行读取后缓存响应内容,以便后续的请求可以直接从缓存中获取响应数据,而不需要重新生成。这对于需要频繁访问相同资源的情况下可以提高性能和减少服务器负载。但是需要注意的一个点是,当使用 ContentCachingResponseWrapper 包装响应对象后,响应的内容会被缓存起来,但是在实际发送响应之前,缓存的内容不会被自动写入到客户端。这时,如果我们希望将缓存的响应内容作为实际的响应返回给客户端,需要使用 copyBodyToResponse() 方法。博主第一次使用时不知道,没有在响应返回前调用copyBodyToResponse()方法,导致接口代码debug亲眼看到接口返回了数据,但是浏览器接收到的数据确是空的诡异现象。
代码示例:
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(req);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(resp);
/*
执行过滤逻辑
*/
// 返回时调用copyBodyToResponse()
responseWrapper.copyBodyToResponse();
}
除此之外,也可以自己手动进行包装,但是没有必要:
public class RepeatableHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final byte[] requestBody;
public RepeatableHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 读取请求体并保存
requestBody = readRequestBody(request);
}
private byte[] readRequestBody(HttpServletRequest request) throws IOException {
ServletInputStream inputStream = request.getInputStream();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
return outputStream.toByteArray();
}
@Override
public ServletInputStream getInputStream() throws IOException {
// 创建一个新的 ServletInputStream,从保存的请求体中读取数据
return new ServletInputStream() {
private int index = 0;
@Override
public int read() throws IOException {
if (index >= requestBody.length) {
return -1;
}
return requestBody[index++];
}
@Override
public boolean isFinished() {
return index >= requestBody.length;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
// 不实现
}
};
}
public String getRequestBodyAsString() {
return new String(requestBody);
}
}