事实证明,尽管
HttpFirewall其中
StrictHttpFirewall包含多个设计错误(在下面的代码中进行了记录),但是几乎不可能逃避Spring
Security的 One True Firewall
并
HttpFirewall通过request属性将信息
HandlerInterceptor传递到,该属性可以将这些标记的请求传递给 真实的
(持久性)防火墙,而不会牺牲最初标记它们的原始业务逻辑。这里记录的方法应该相当面向未来,因为它符合来自
HttpFirewall接口的简单协定,其余的只是核心Spring
framework和Java Servlet API。
从本质上讲,这是我先前回答的更复杂但更完整的选择。在此答案中,我实现了一个新的子类,
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;public class AnnotatingHttpFirewall extends StrictHttpFirewall{ public static final String HTTP_HEADER_REQUEST_REJECTED_FLAG = "X-HttpFirewall-RequestRejectedFlag"; public static final String HTTP_HEADER_REQUEST_REJECTED_REASON = "X-HttpFirewall-RequestRejectedReason"; private static final Logger LOGGER = Logger.getLogger(AnnotatingHttpFirewall.class.getName()); public AnnotatingHttpFirewall() { super(); return; } @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; } }; } } @Override public HttpServletResponse getFirewalledResponse(final HttpServletResponse response) { // Note: The FirewalledResponse class is not accessible outside the package. return super.getFirewalledResponse(response); } 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。
@EnableWebSecuritypublic class WebSecurityConfig extends WebSecurityConfigurerAdapter{ 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之所以走a之路,是因为它似乎提供了最大的灵活性,并且可以直接在Spring
framework中工作。
RequestBlockedException.java
此自定义异常可以由错误控制器处理。可以扩展它以添加可与应用程序业务逻辑(例如,持久防火墙)相关的原始请求(甚至是完整请求本身)中可用的任何请求标头,参数或属性。
public class RequestBlockedException extends RuntimeException{ private static final long serialVersionUID = 1L; private String requestUrl; private String remoteAddress; private String reason; private String userAgent; 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; } public String getRequestUrl() { return this.requestUrl; } public String getRemoteAddress() { return this.remoteAddress; } public String getUserAgent() { return this.userAgent; } 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;public final class FirewallInterceptor implements HandlerInterceptor{ 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到注册表。
@EnableWebMvcpublic class WebConfig extends WebMvcConfigurerAdapter{ @Override public void addInterceptors(final InterceptorRegistry registry) { // Register firewall interceptor for all URLs in webapp. registry.addInterceptor(new FirewallInterceptor()).addPathPatterns(" private static final Logger LOGGER = Logger.getLogger(ErrorController.class.getName()); @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 pre. } @ExceptionHandler(NoHandlerFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public String handleException(final NoHandlerFoundException ex) { return "notFoundPage"; }}FirewallController.java
具有默认映射并抛出的控制器
NoHandlerFoundException。这规避了DispatcherServlet.noHandlerFound中的先有其后的策略,允许该方法
始终
查找映射,以便
FirewallInterceptor.preHandle始终对其进行调用。这使
RequestRejectedByFirewallException优先
NoHandlerFoundException。
为什么需要这样做:
如所提到的在这里,当
NoHandlerFoundException从抛出
DispatcherServlet(即,当一个请求的URL不具有相应的映射),也没有办法来处理从上面的防火墙生成的异常(
NoHandlerFoundException被调用之前抛出preHandle()
,所以这些请求会)进入404视图(在我的情况下,这不是您想要的行为-
您会看到很多“找不到使用URI的HTTP请求的映射…”消息)。可以通过将特殊标头的检查移到
noHandlerFound方法中来解决此问题。不幸的是,如果没有从头开始编写新的Dispatcher
Servlet,就无法做到这一点,那么您最好扔掉整个Spring框架。不可能扩展
DispatcherServlet由于受保护的方法,私有方法和最终方法的混合使用,并且其属性不可访问(无getter或setter)。包装类也是不可能的,因为没有可以实现的通用接口。此类中的默认映射提供了一种优雅的方式来规避所有这些逻辑。
重要说明
:下面的RequestMapping将阻止解析静态资源,因为它优先于所有已注册的ResourceHandler。我仍在寻找解决方法,但是一种可能是尝试使用此答案中建议的处理静态资源的方法之一。
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;@Controllerpublic final class FirewallController{ protected static final String REQUEST_URL = "requestUrl"; protected static final String REQUEST_METHOD = "requestMethod"; protected static final String REQUEST_HEADERS = "requestHeaders"; public FirewallController() { return; } @ModelAttribute(REQUEST_URL) public final String getRequestURL(final HttpServletRequest request) { return request.getRequestURL().toString(); } @ModelAttribute(REQUEST_METHOD) public final String getRequestMethod(final HttpServletRequest request) { return request.getMethod(); } @ModelAttribute(REQUEST_HEADERS) public final HttpHeaders getRequestHeaders(final HttpServletRequest request) { return FirewallController.headers(request); } @RequestMapping(value = " 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; }}结果
When both parts of this are working, you’ll see the following two warnings
logged (the first one is in Spring Security, the second one is the Spring
framework (Core)
ErrorController). Now you have full control over logging,
and an extensible application firewall that you can adjust however you need.
Sep 12, 2018 10:24:37 AM com.mycompany.spring.security.AnnotatingHttpFirewall getFirewalledRequestWARNING: 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.phpSep 12, 2018 10:24:37 AM com.mycompany.spring.controller.ErrorController handleExceptionWARNING: 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.



