如何在 Spring 中拦截 RequestRejectedException?

Posted

技术标签:

【中文标题】如何在 Spring 中拦截 RequestRejectedException?【英文标题】:How to intercept a RequestRejectedException in Spring? 【发布时间】:2019-01-18 04:51:44 【问题描述】:

我在我的 Tomcat 日志中看到 大量 RequestRejectedException 条目(示例粘贴在下面)。在几个月前的次要版本升级(Spring Security 4.2.4,IIRC)之后,这些开始出现在我的日志文件中,因此这显然是 Spring 中默认启用的新安全功能。一个类似的问题是reported here,但我的问题具体涉及如何在控制器中拦截这些异常。有一个针对此问题的 Spring Security 错误记录 (Provide a way to handle RequestRejectedException)。但是,他们直到 Spring 5.1 才针对此问题进行修复。

我了解why these exceptions are being thrown,我不想disable this security feature。

我想对该功能进行一些控制,以便:

    我知道我没有阻止合法用户访问我的网站。 我可以看到触发此事件的请求(它们是 SQL 注入攻击吗?) 我可以调整服务器响应。 Spring Security 防火墙将完整的堆栈跟踪转储到 Web 客户端(信息泄露)以及 500 Internal Server Error(这是非常不正确的,这应该是 400 Bad Request)。

我想找到一种方法来记录所请求的 URL,但还要抑制专门针对这些异常的堆栈跟踪,因为它们正在污染我的日志文件而没有给我任何有用的信息。最理想的情况是,我想拦截这些异常并在我的应用程序层中处理它们,而不是在 Tomcat 日志中报告它们。

例如,这是每天出现在我的catalina.out 中的数千条日志条目之一:

Aug 10, 2018 2:01:36 PM org.apache.catalina.core.StandardWrapperValve invoke
SEVERE: Servlet.service() for servlet [dispatcher] in context with path [] threw exception
org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentially malicious String ";"
        at org.springframework.security.web.firewall.StrictHttpFirewall.rejectedBlacklistedUrls(StrictHttpFirewall.java:265)
        at org.springframework.security.web.firewall.StrictHttpFirewall.getFirewalledRequest(StrictHttpFirewall.java:245)
        at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:193)
        at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:177)
        at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:347)
        at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:263)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
        at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198)
        at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
        at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:496)
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81)
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
        at org.apache.coyote.ajp.AjpProcessor.service(AjpProcessor.java:486)
        at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:790)
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1459)
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:748)

我在两天内看到了超过 3,200 个这样的问题,并且它很快成为我的catalina.out 日志文件的最大贡献者,以至于它阻止我看到其他合法问题。从本质上讲,这个新的 Spring Security 特性是一种内置的拒绝服务形式,自 4 月以来它已经浪费了我几个小时的时间。我并不是说它不是一个重要的特性,只是默认实现完全拙劣,我想找到一种方法来控制它,无论是作为开发人员还是作为系统管理员。

我使用自定义错误控制器来拦截 Spring 中的许多其他异常类型(包括IOException)。但是,RequestRejectedException 似乎由于某种原因而失败了。

这是我的ErrorController.java 的相关部分,以便了解我想要完成的工作:

@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 HttpFirewall.
     *
     * @param ex A RequestRejectedException exception.
     * @return The tile definition name for the page.
     */
    @ExceptionHandler(RequestRejectedException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String handleRequestRejectedException(final HttpServletRequest request, final RequestRejectedException ex)
    
        if (LOGGER.isLoggable(Level.INFO))
        
            LOGGER.log(Level.INFO, "Request Rejected", ex);
        

        LOGGER.log(Level.WARNING, "Rejected request for [" + request.getRequestURL().toString() + "]. Reason: " + ex.getMessage());
        return "errorPage";
    

    /**
     * Generates a Server Error page.
     *
     * @param ex An exception.
     * @return The tile definition name for the page.
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public String handleException(final Exception ex)
    
        if (LOGGER.isLoggable(Level.SEVERE))
        
            LOGGER.log(Level.SEVERE, "Server Error", ex);
        

        return "errorPage";
    

此错误控制器适用于许多异常。比如它成功拦截了这个IllegalStateException

Aug 05, 2018 7:50:30 AM com.mycompany.spring.controller.ErrorController handleException
SEVERE: Server Error
java.lang.IllegalStateException: Cannot create a session after the response has been committed
        at org.apache.catalina.connector.Request.doGetSession(Request.java:2999)
...

但是,这并没有拦截RequestRejectedException(如上面第一个日志示例中缺少“服务器错误”所示)。

如何在错误控制器中拦截RequestRejectedException

【问题讨论】:

我不知道是谁添加了额外的斜线?我在浏览器的地址字段中输入“example.com/app/main”,Spring 不知何故得到了“/app//main”。 记录为Spring Security bug 5007。已在 2020 年 7 月的 5.4.0-M1 版本中注明; version 5.4.0 was released 2020 年 9 月。 但是,我在使用 Spring Security 5.4.2 时遇到了同样的问题。所以我猜他们并没有太多修复问题,而是更容易实施变通方法。 【参考方案1】:

我们的是使用 spring-security-core 的 spring-webmvc (4.3.25.RELEASE) GUI (4.2.13.RELEASE),问题是由于 url 字符串包含 ";jsessionid=D3A0470674704B75756AA10F50AA2CFC" 并以分号作为其参数之一。

错误org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentially malicious String ";" 仅在第一次加载网页时出现,导致各种 CSS 格式问题,图像未加载、颜色和字体设置不正确。但是,在刷新同一页面或单击任何导航链接后,下一页可以正常加载,并且所有 CSS 都已正确实现。此外,这个错误RequestRejectedException 严重污染了日志。

我们希望处理这个问题,以便在创建新会话时,生成的 cookie 处理带有 jsessionid 的会话,而不是像第二次以后那样处理查询字符串。

我的解决方案源自于在https://***.com/a/52635656/2915705 中实现上述过滤器,但不是将其发送到错误页面检查会话并将其重定向到编码 URL,如 https://***.com/a/4019476/2915705 中所述。在此解决方案之后,即使是新会话或首次页面加载,我们也从未收到 RequestRejectedException 或 CSS 问题。

更新后的LogAndSuppressRequestRejectedExceptionFilter见下方

@Component("logAndSuppressRequestRejectedExceptionFilter")
@Order(Ordered.HIGHEST_PRECEDENCE)
public class LogAndSuppressRequestRejectedExceptionFilter extends GenericFilterBean 
    private static final Logger logger = LoggerFactory.getLogger(LogAndSuppressRequestRejectedExceptionFilter.class);

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException 
        try 
            HttpServletRequest httpRequest = (HttpServletRequest) req;
            HttpServletResponse httpResponse = (HttpServletResponse) res;
            HttpSession session = httpRequest.getSession();

            if (session.isNew()) 
                // New session? OK, redirect to encoded URL with jsessionid in it (and
                // implicitly also set cookie).
                logger.debug("New session - redirect to encoded url");
                httpResponse.sendRedirect(httpResponse.encodeRedirectURL(httpRequest.getRequestURI()));
                return;
             else if (session.getAttribute("verified") == null) 
                // Session has not been verified yet? OK, mark it verified so that we don't need
                // to repeat this.
                logger.debug("Setting session to verified");
                session.setAttribute("verified", true);
                if (httpRequest.isRequestedSessionIdFromCookie()) 
                    // Supports cookies? OK, redirect to unencoded URL to get rid of jsessionid in
                    // URL.
                    logger.debug("redirect to unencoded URL to get rid of jsessionid in url");
                    httpResponse.sendRedirect(httpRequest.getRequestURI().split(";")[0]);
                    return;
                
            

            chain.doFilter(req, res);

         catch (RequestRejectedException ex) 
            HttpServletRequest request = (HttpServletRequest) req;
            logger.warn("request_rejected: remote=, user_agent=, request_url=", request.getRemoteHost(),
                    request.getHeader(HttpHeaders.USER_AGENT), request.getRequestURL(), ex.getMessage());
            return;
        
    

【讨论】:

【参考方案2】:

我在this commit 的最近 github 更改中看到了一些可行的解决方案

如果您注册 RequestRejectedHandler 类型的 bean,它应该可以工作,或者如我所见,还将通过 WebSecurityWebSecurityConfigurerAdapter 中进行集成。不幸的是,使用依赖管理的 2.3.3.RELEASE 中可能不包含此更改。它应该存在于 Spring Security Config 5.4.0-M1 中。依赖管理,2.4.0-M1版本。

迟早,遇到此答案的人应该会在标准版本中看到此更改。

【讨论】:

感谢您的更新。我将推迟迁移到 Spring 5,直到它包含在版本中。与此同时,我的上述策略(参见接受的解决方案)已经完美运行了 2 年多。【参考方案3】:

对于 5.4 及以上的 Spring 安全版本,您可以简单地创建一个 RequestRejectedHandler 类型的 bean,该 bean 将被注入 Spring 安全过滤器链中

import org.springframework.security.web.firewall.RequestRejectedHandler;
import org.springframework.security.web.firewall.HttpStatusRequestRejectedHandler;

@Bean
RequestRejectedHandler requestRejectedHandler() 
   // sends an error response with a configurable status code (default is 400 BAD_REQUEST)
   // we can pass a different value in the constructor
   return new HttpStatusRequestRejectedHandler();

【讨论】:

似乎 RequestRejectedHandler 仅在 spring security 5.4 中添加:github.com/spring-projects/spring-security/blob/master/web/src/…@since 5.4 @ARK 是的,你是对的,我会调整它。谢谢!【参考方案4】:

一个很简单的方法是使用web.xml;在该文件中指定错误页面:

<error-page>
  <exception-type>org.springframework.security.web.firewall.RequestRejectedException</exception-type>
  <location>/request-rejected</location>
</error-page>

对于指定的路径(位置),在@Controller-annotated类中添加映射:

@RequestMapping(value = "/request-rejected")
@ResponseStatus(HttpStatus.BAD_REQUEST)
public @ResponseBody String handleRequestRejected(
        @RequestAttribute(RequestDispatcher.ERROR_EXCEPTION) RequestRejectedException ex,
        @RequestAttribute(RequestDispatcher.ERROR_REQUEST_URI) String uri) 

    String msg = ex.getMessage();

    // optionally log the message and requested URI (slf4j)
    logger.warn("Request with URI [] rejected. ", uri, msg);

    return msg;

【讨论】:

【参考方案5】:

另一种处理方法是使用Spring AOP。我们可以围绕 FilterChainProxy.doFilter() 方法创建一个通知,该方法捕获 HttpFirewall 抛出的任何 RequestRejectedException(s) 并将其转换为 400 BAD_REQUEST

@Aspect
@Component
public class FilterChainProxyAdvice 

    @Around("execution(public void org.springframework.security.web.FilterChainProxy.doFilter(..))")
    public void handleRequestRejectedException (ProceedingJoinPoint pjp) throws Throwable 
        try 
            pjp.proceed();
         catch (RequestRejectedException exception) 
            HttpServletResponse response = (HttpServletResponse) pjp.getArgs()[1]);
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
        
    

【讨论】:

这被调用了两次 @SabareeshKkanan 这是由于 AspectJ 通知的默认通知调用。 handleRequestRejectedException() 方法被调用两次是正确的,但FilterChainProxy.doFilter() 的实际方法调用只被调用一次。查看here 以了解更多@Around 拦截的工作原理。 似乎在 Spring 5.2.9 上不起作用,NPE 在上下文设置期间被抛出GenericFilterBean#init 方法,因为Log logger 字段由于某些未知原因为空【参考方案6】:

也可以通过简单的过滤器来处理,会导致404错误响应

@Component
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
public class LogAndSuppressRequestRejectedExceptionFilter extends GenericFilterBean 

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException 
        try 
            chain.doFilter(req, res);
         catch (RequestRejectedException e) 
            HttpServletRequest request = (HttpServletRequest) req;
            HttpServletResponse response = (HttpServletResponse) res;

            log
                .warn(
                        "request_rejected: remote=, user_agent=, request_url=",
                        request.getRemoteHost(),  
                        request.getHeader(HttpHeaders.USER_AGENT),
                        request.getRequestURL(), 
                        e
                );

            response.sendError(HttpServletResponse.SC_NOT_FOUND);
        
    

【讨论】:

这是一个非常不受欢迎的答案。感谢您提供简单的解决方案! 这可行,但我们得到的是错误日志而不是警告日志 简单、有效。感谢分享! 这对我有用,除非我必须更改日志方法,否则仍然会写入堆栈跟踪。更改为:log.warn("request_rejected: remote=, user_agent=, request_url= reason=", request.getRemoteHost(), request.getHeader(HttpHeaders.USER_AGENT), request.getRequestURL() , e.getMessage());【参考方案7】:

事实证明,尽管 HttpFirewallStrictHttpFirewall 包含几个设计错误(记录在下面的代码中),但几乎不可能逃脱 Spring Security 的 One True Firewall 并隧道 @ 987654328@ 信息通过请求属性传递给HandlerInterceptorHandlerInterceptor 可以将这些标记的请求传递到真实(持久)防火墙,而不会牺牲最初标记它们的原始业务逻辑。这里记录的方法应该是面向未来的,因为它符合来自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 框架

这个解决方案的第二部分可以想象为ServletFilterHandlerInterceptor。我要走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.

【讨论】:

【参考方案8】:

我实现了StrictHttpFirewall 的一个子类,它将请求信息记录到控制台并抛出一个带有抑制堆栈跟踪的新异常。这部分解决了我的问题(至少我现在可以看到错误的请求)。

如果您只想在没有堆栈跟踪的情况下查看被拒绝的请求,这就是您正在寻找的答案。

如果您想在控制器中处理这些异常,请参阅accepted answer 以获得完整(但稍微复杂一些)的解决方案。


LoggingHttpFirewall.java

此类扩展 StrictHttpFirewall 以捕获 RequestRejectedException 并引发新异常,其中包含来自请求的元数据和抑制的堆栈跟踪。

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 final class LoggingHttpFirewall extends StrictHttpFirewall

    /**
     * Logger.
     */
    private static final Logger LOGGER = Logger.getLogger(LoggingHttpFirewall.class.getName());

    /**
     * Default constructor.
     */
    public LoggingHttpFirewall()
    
        super();
        return;
    

    /**
     * Provides the request object which will be passed through the filter chain.
     *
     * @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.
     * @throws RequestRejectedException if the request should be rejected immediately.
     */
    @Override
    public FirewalledRequest getFirewalledRequest(final HttpServletRequest request) throws RequestRejectedException
    
        try
        
            return super.getFirewalledRequest(request);
         catch (RequestRejectedException ex) 
            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());
            

            // Wrap in a new RequestRejectedException with request metadata and a shallower stack trace.
            throw new RequestRejectedException(ex.getMessage() + ".\n Remote Host: " + request.getRemoteHost() + "\n User Agent: " + request.getHeader("User-Agent") + "\n Request URL: " + request.getRequestURL().toString())
            
                private static final long serialVersionUID = 1L;

                @Override
                public synchronized Throwable fillInStackTrace()
                
                    return this; // suppress the stack trace.
                
            ;
        
    

    /**
     * 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);
    


WebSecurityConfig.java

WebSecurityConfig中,将HTTP防火墙设置为LoggingHttpFirewall

@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 LoggingHttpFirewall()); // Set the custom firewall.
        return;
    


结果

将此解决方案部署到生产环境后,我很快发现StrictHttpFirewall 的默认行为是阻止 Google 将我的网站编入索引!

Aug 13, 2018 1:48:56 PM com.mycompany.spring.security.AnnotatingHttpFirewall getFirewalledRequest
WARNING: Intercepted RequestBlockedException: Remote Host: 66.249.64.223 User Agent: Mozilla/5.0 (Linux; android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (Khtml, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html) Request URL: https://www.mycompany.com/10.1601/tx.3784;jsessionid=692804549F9AB55F45DBD0AFE2A97FFD

我一发现这一点,就迅速部署了一个新版本(包含在my other answer 中),它查找;jsessionid= 并允许这些请求通过。很可能还有其他请求也应该通过,现在我有办法检测到这些。

【讨论】:

以上是关于如何在 Spring 中拦截 RequestRejectedException?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Spring WebClient 中拦截 http 流量?

Spring mvc 3:如何在拦截器中获取路径变量?

Spring Boot项目中如何定制拦截器

spring 中如何进行方法的判定

如何拦截 Spring Interceptor 到 Node Service 的请求

如何在 spring-boot 中拦截嵌入式 Tomcat 上的“全局”404