事实证明,尽管 HttpFirewall 和 StrictHttpFirewall 包含几个设计错误(记录在下面的代码中),但几乎不可能逃脱 Spring Security 的 One True Firewall 并隧道 @ 987654328@ 信息通过请求属性传递给HandlerInterceptor,HandlerInterceptor 可以将这些标记的请求传递到真实(持久)防火墙,而不会牺牲最初标记它们的原始业务逻辑。这里记录的方法应该是面向未来的,因为它符合来自HttpFirewall 接口的简单契约,其余的只是核心 Spring Framework 和 Java Servlet API。
这本质上是my earlier answer 的更复杂但更完整的替代方案。在这个答案中,我实现了 StrictHttpFirewall 的一个新子类,它在特定的日志记录级别拦截和记录被拒绝的请求,但还向 HTTP 请求添加了一个属性,将其标记为下游过滤器(或控制器)处理。此外,这个AnnotatingHttpFirewall 提供了一个inspect() 方法,允许子类添加自定义规则来阻止请求。
此解决方案分为两部分:(1) Spring Security 和 (2) Spring Framework (Core),因为这是导致此问题的分水岭首先,这显示了如何桥接它。
作为参考,这是在 Spring 4.3.17 和 Spring Security 4.2.6 上测试的。 Spring 5.1 发布时可能会有重大变化。
第 1 部分:Spring 安全性
这是在 Spring Security 中执行日志记录和标记的解决方案的一半。
AnnotatingHttpFirewall.java
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.RequestRejectedException;
import org.springframework.security.web.firewall.StrictHttpFirewall;
/**
* Overrides the StrictHttpFirewall to log some useful information about blocked requests.
*/
public class AnnotatingHttpFirewall extends StrictHttpFirewall
{
/**
* The name of the HTTP header representing a request that has been rejected by this firewall.
*/
public static final String HTTP_HEADER_REQUEST_REJECTED_FLAG = "X-HttpFirewall-RequestRejectedFlag";
/**
* The name of the HTTP header representing the reason a request has been rejected by this firewall.
*/
public static final String HTTP_HEADER_REQUEST_REJECTED_REASON = "X-HttpFirewall-RequestRejectedReason";
/**
* Logger.
*/
private static final Logger LOGGER = Logger.getLogger(AnnotatingHttpFirewall.class.getName());
/**
* Default constructor.
*/
public AnnotatingHttpFirewall()
{
super();
return;
}
/**
* Provides the request object which will be passed through the filter chain.
*
* @param request The original HttpServletRequest.
* @returns A FirewalledRequest (required by the HttpFirewall interface) which
* inconveniently breaks the general contract of ServletFilter because
* we can't upcast this to an HttpServletRequest. This prevents us
* from re-wrapping this using an HttpServletRequestWrapper.
*/
@Override
public FirewalledRequest getFirewalledRequest(final HttpServletRequest request)
{
try
{
this.inspect(request); // Perform any additional checks that the naive "StrictHttpFirewall" misses.
return super.getFirewalledRequest(request);
} catch (RequestRejectedException ex) {
final String requestUrl = request.getRequestURL().toString();
// Override some of the default behavior because some requests are
// legitimate.
if (requestUrl.contains(";jsessionid="))
{
// Do not block non-cookie serialized sessions. Google's crawler does this often.
} else {
// Log anything that is blocked so we can find these in the catalina.out log.
// This will give us any information we need to make
// adjustments to these special cases and see potentially
// malicious activity.
if (LOGGER.isLoggable(Level.WARNING))
{
LOGGER.log(Level.WARNING, "Intercepted RequestBlockedException: Remote Host: " + request.getRemoteHost() + " User Agent: " + request.getHeader("User-Agent") + " Request URL: " + request.getRequestURL().toString());
}
// Mark this request as rejected.
request.setAttribute(HTTP_HEADER_REQUEST_REJECTED, Boolean.TRUE);
request.setAttribute(HTTP_HEADER_REQUEST_REJECTED_REASON, ex.getMessage());
}
// Suppress the RequestBlockedException and pass the request through
// with the additional attribute.
return new FirewalledRequest(request)
{
@Override
public void reset()
{
return;
}
};
}
}
/**
* Provides the response which will be passed through the filter chain.
* This method isn't extensible because the request may already be committed.
* Furthermore, this is only invoked for requests that were not blocked, so we can't
* control the status or response for blocked requests here.
*
* @param response The original HttpServletResponse.
* @return the original response or a replacement/wrapper.
*/
@Override
public HttpServletResponse getFirewalledResponse(final HttpServletResponse response)
{
// Note: The FirewalledResponse class is not accessible outside the package.
return super.getFirewalledResponse(response);
}
/**
* Perform any custom checks on the request.
* This method may be overridden by a subclass in order to supplement or replace these tests.
*
* @param request The original HttpServletRequest.
* @throws RequestRejectedException if the request should be rejected immediately.
*/
public void inspect(final HttpServletRequest request) throws RequestRejectedException
{
final String requestUri = request.getRequestURI(); // path without parameters
// final String requestUrl = request.getRequestURL().toString(); // full path with parameters
if (requestUri.endsWith("/wp-login.php"))
{
throw new RequestRejectedException("The request was rejected because it is a vulnerability scan.");
}
if (requestUri.endsWith(".php"))
{
throw new RequestRejectedException("The request was rejected because it is a likely vulnerability scan.");
}
return; // The request passed all custom tests.
}
}
WebSecurityConfig.java
在WebSecurityConfig中,将HTTP防火墙设置为AnnotatingHttpFirewall。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
/**
* Default constructor.
*/
public WebSecurityConfig()
{
super();
return;
}
@Override
public final void configure(final WebSecurity web) throws Exception
{
super.configure(web);
web.httpFirewall(new AnnotatingHttpFirewall()); // Set the custom firewall.
return;
}
}
第 2 部分:Spring 框架
这个解决方案的第二部分可以想象为ServletFilter 或HandlerInterceptor。我要走HandlerInterceptor 的道路,因为它似乎提供了最大的灵活性并且直接在 Spring 框架中工作。
RequestBlockedException.java
此自定义异常可由错误控制器处理。这可以扩展为添加原始请求(甚至是完整请求本身)中可用的任何请求标头、参数或属性,这些请求可能与应用程序业务逻辑相关(例如,持久防火墙)。
/**
* A custom exception for situations where a request is blocked or rejected.
*/
public class RequestBlockedException extends RuntimeException
{
private static final long serialVersionUID = 1L;
/**
* The requested URL.
*/
private String requestUrl;
/**
* The remote address of the client making the request.
*/
private String remoteAddress;
/**
* A message or reason for blocking the request.
*/
private String reason;
/**
* The user agent supplied by the client the request.
*/
private String userAgent;
/**
* Creates a new Request Blocked Exception.
*
* @param reqUrl The requested URL.
* @param remoteAddr The remote address of the client making the request.
* @param userAgent The user agent supplied by the client making the request.
* @param message A message or reason for blocking the request.
*/
public RequestBlockedException(final String reqUrl, final String remoteAddr, final String userAgent, final String message)
{
this.requestUrl = reqUrl;
this.remoteAddress = remoteAddr;
this.userAgent = userAgent;
this.reason = message;
return;
}
/**
* Gets the requested URL.
*
* @return A URL.
*/
public String getRequestUrl()
{
return this.requestUrl;
}
/**
* Gets the remote address of the client making the request.
*
* @return A remote address.
*/
public String getRemoteAddress()
{
return this.remoteAddress;
}
/**
* Gets the user agent supplied by the client making the request.
*
* @return A user agent string.
*/
public String getUserAgent()
{
return this.userAgent;
}
/**
* Gets the reason for blocking the request.
*
* @return A message or reason for blocking the request.
*/
public String getReason()
{
return this.reason;
}
}
FirewallInterceptor.java
这个拦截器在 Spring Security 过滤器运行后被调用(即,在AnnotatingHttpFirewall 标记了应该被拒绝的请求之后。这个拦截器检测到请求中的那些标志(属性)并引发一个自定义异常,我们的错误控制器可以处理。
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
/**
* Intercepts requests that were flagged as rejected by the firewall.
*/
public final class FirewallInterceptor implements HandlerInterceptor
{
/**
* Default constructor.
*/
public FirewallInterceptor()
{
return;
}
@Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception
{
if (Boolean.TRUE.equals(request.getAttribute(AnnotatingHttpFirewall.HTTP_HEADER_REQUEST_REJECTED)))
{
// Throw a custom exception that can be handled by a custom error controller.
final String reason = (String) request.getAttribute(AnnotatingHttpFirewall.HTTP_HEADER_REQUEST_REJECTED_REASON);
throw new RequestRejectedByFirewallException(request.getRequestURL().toString(), request.getRemoteAddr(), request.getHeader(HttpHeaders.USER_AGENT), reason);
}
return true; // Allow the request to proceed normally.
}
@Override
public void postHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final ModelAndView modelAndView) throws Exception
{
return;
}
@Override
public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final Exception ex) throws Exception
{
return;
}
}
WebConfig.java
在WebConfig 中,将FirewallInterceptor 添加到注册表中。
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter
{
/**
* Among your other methods in this class, make sure you register
* your Interceptor.
*/
@Override
public void addInterceptors(final InterceptorRegistry registry)
{
// Register firewall interceptor for all URLs in webapp.
registry.addInterceptor(new FirewallInterceptor()).addPathPatterns("/**");
return;
}
}
ErrorController.java
这专门处理上面的自定义异常,并为客户端生成一个干净的错误页面,同时记录所有相关信息并为自定义应用程序防火墙调用任何特殊业务逻辑。
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import RequestBlockedException;
@ControllerAdvice
public final class ErrorController
{
/**
* Logger.
*/
private static final Logger LOGGER = Logger.getLogger(ErrorController.class.getName());
/**
* Generates an Error page by intercepting exceptions generated from AnnotatingHttpFirewall.
*
* @param request The original HTTP request.
* @param ex A RequestBlockedException exception.
* @return The tile definition name for the page.
*/
@ExceptionHandler(RequestBlockedException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String handleRequestBlockedException(final RequestBlockedException ex)
{
if (LOGGER.isLoggable(Level.WARNING))
{
LOGGER.log(Level.WARNING, "Rejected request from " + ex.getRemoteAddress() + " for [" + ex.getRequestUrl() + "]. Reason: " + ex.getReason());
}
// Note: Perform any additional business logic or logging here.
return "errorPage"; // Returns a nice error page with the specified status code.
}
/**
* Generates a Page Not Found page.
*
* @param ex A NoHandlerFound exception.
* @return The tile definition name for the page.
*/
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public String handleException(final NoHandlerFoundException ex)
{
return "notFoundPage";
}
}
FirewallController.java
具有默认映射的控制器会抛出NoHandlerFoundException。
这绕过了DispatcherServlet.noHandlerFound 中的先有鸡还是先有蛋的策略,允许该方法始终 找到映射,以便始终调用FirewallInterceptor.preHandle。这使RequestRejectedByFirewallException 优先于NoHandlerFoundException。
为什么这是必要的:
如here所说,当DispatcherServlet抛出一个NoHandlerFoundException时(即请求的URL没有对应的映射),没有办法处理上述防火墙产生的异常(NoHandlerFoundException在调用 preHandle() 之前抛出),因此这些请求将落入您的 404 视图(在我的情况下,这不是所需的行为 - 您将看到很多“没有为带有 URI 的 HTTP 请求找到映射......”消息)。这可以通过将特殊标头的检查移动到 noHandlerFound 方法中来解决。不幸的是,如果不从头开始编写一个新的 Dispatcher Servlet,就没有办法做到这一点,然后你还不如扔掉整个 Spring Framework。由于受保护、私有和最终方法的混合,以及其属性不可访问(没有 getter 或 setter)的事实,不可能扩展 DispatcherServlet。包装类也是不可能的,因为没有可以实现的通用接口。此类中的默认映射提供了一种优雅的方式来规避所有这些逻辑。
重要警告:下面的 RequestMapping 将阻止静态资源的解析,因为它优先于所有已注册的 ResourceHandler。我仍在寻找解决方法,但一种可能是尝试this answer 中建议的处理静态资源的方法之一。
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.NoHandlerFoundException;
@Controller
public final class FirewallController
{
/**
* The name of the model attribute (or request parameter for advertisement click tracking) that contains the request URL.
*/
protected static final String REQUEST_URL = "requestUrl";
/**
* The name of the model attribute that contains the request method.
*/
protected static final String REQUEST_METHOD = "requestMethod";
/**
* The name of the model attribute that contains all HTTP headers.
*/
protected static final String REQUEST_HEADERS = "requestHeaders";
/**
* Default constructor.
*/
public FirewallController()
{
return;
}
/**
* Populates the request URL model attribute from the HTTP request.
*
* @param request The HTTP request.
* @return The request URL.
*/
@ModelAttribute(REQUEST_URL)
public final String getRequestURL(final HttpServletRequest request)
{
return request.getRequestURL().toString();
}
/**
* Populates the request method from the HTTP request.
*
* @param request The HTTP request.
* @return The request method (GET, POST, HEAD, etc.).
*/
@ModelAttribute(REQUEST_METHOD)
public final String getRequestMethod(final HttpServletRequest request)
{
return request.getMethod();
}
/**
* Gets all headers from the HTTP request.
*
* @param request The HTTP request.
* @return The request headers.
*/
@ModelAttribute(REQUEST_HEADERS)
public final HttpHeaders getRequestHeaders(final HttpServletRequest request)
{
return FirewallController.headers(request);
}
/**
* A catch-all default mapping that throws a NoHandlerFoundException.
* This will be intercepted by the ErrorController, which allows preHandle to work normally.
*
* @param requestMethod The request method.
* @param requestUrl The request URL.
* @param requestHeaders The request headers.
* @throws NoHandlerFoundException every time this method is invoked.
*/
@RequestMapping(value = "/**") // NOTE: This prevents resolution of static resources. Still looking for a workaround for this.
public void getNotFoundPage(@ModelAttribute(REQUEST_METHOD) final String requestMethod, @ModelAttribute(REQUEST_URL) final String requestUrl, @ModelAttribute(REQUEST_HEADERS) final HttpHeaders requestHeaders) throws NoHandlerFoundException
{
throw new NoHandlerFoundException(requestMethod, requestUrl, requestHeaders);
}
/**
* Gets all headers from a HTTP request.
*
* @param request The HTTP request.
* @return The request headers.
*/
public static HttpHeaders headers(final HttpServletRequest request)
{
final HttpHeaders headers = new HttpHeaders();
for (Enumeration<?> names = request.getHeaderNames(); names.hasMoreElements();)
{
final String headerName = (String) names.nextElement();
for (Enumeration<?> headerValues = request.getHeaders(headerName); headerValues.hasMoreElements();)
{
headers.add(headerName, (String) headerValues.nextElement());
}
}
return headers;
}
}
结果
当这两个部分都工作时,您会看到以下两个警告记录(第一个是在 Spring Security 中,第二个是 Spring Framework (Core) ErrorController)。现在您可以完全控制日志记录,以及可以根据需要进行调整的可扩展应用程序防火墙。
Sep 12, 2018 10:24:37 AM com.mycompany.spring.security.AnnotatingHttpFirewall getFirewalledRequest
WARNING: Intercepted org.springframework.security.web.firewall.RequestRejectedException: Remote Host: 0:0:0:0:0:0:0:1 User Agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64; rv:62.0) Gecko/20100101 Firefox/62.0 Request URL: http://localhost:8080/webapp-www-mycompany-com/login.php
Sep 12, 2018 10:24:37 AM com.mycompany.spring.controller.ErrorController handleException
WARNING: Rejected request from 0:0:0:0:0:0:0:1 for [http://localhost:8080/webapp-www-mycompany-com/login.php]. Reason: The request was rejected because it is a likely vulnerability scan.