结果是尽管
HttpFirewall
和
StrictHttpFirewall
包含几个设计错误(记录在下面的代码中),很难逃脱Spring Security的
一个真正的防火墙
在隧道里
http防火墙
通过请求属性向
HandlerInterceptor
可以将这些标记的请求传递给
真实的
(持久)防火墙,而不牺牲最初标记它们的原始业务逻辑。这里记录的方法应该是相当未来的证明,因为它符合
http防火墙
接口,其余的只是核心Spring框架和Java Servlet API。
这基本上是一个更复杂但更完整的替代方案
my earlier answer
. 在这个答案中,我实现了
严格的防火墙
它在特定的日志级别截取并记录被拒绝的请求,同时还向HTTP请求添加一个属性,将其标记为要由下游筛选器(或控制器)处理。还有,这个
AnnotatingHttpFirewall
提供
inspect()
方法,该方法允许子类为阻止请求添加自定义规则。
这个解决方案分为两部分:(1)
春季安全
和(2)
弹簧骨架(芯)
,因为这是导致这个问题的第一个原因,这说明了如何弥合它。
作为参考,在弹簧4.3.17和弹簧安全4.2.6上进行了测试。当Spring5.1发布时,可能会有显著的变化。
第一部分:弹簧安全
这是在Spring Security中执行日志记录和标记的解决方案的一半。
注释httpfirewall.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);
return super.getFirewalledRequest(request);
} catch (RequestRejectedException ex) {
final String requestUrl = request.getRequestURL().toString();
if (requestUrl.contains(";jsessionid="))
{
} else {
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());
}
request.setAttribute(HTTP_HEADER_REQUEST_REJECTED, Boolean.TRUE);
request.setAttribute(HTTP_HEADER_REQUEST_REJECTED_REASON, ex.getMessage());
}
return new FirewalledRequest(request)
{
@Override
public void reset()
{
return;
}
};
}
}
@Override
public HttpServletResponse getFirewalledResponse(final HttpServletResponse response)
{
return super.getFirewalledResponse(response);
}
public void inspect(final HttpServletRequest request) throws RequestRejectedException
{
final String requestUri = request.getRequestURI();
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;
}
}
WebSecurity配置.java
在
WebSecurityConfig
,将HTTP防火墙设置为
注释httpfirewall
.
@EnableWebSecurity
public 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());
return;
}
}
第2部分:弹簧框架
这个解决方案的第二部分可以想象为
ServletFilter
或
拦截器
. 我要走一条
拦截器
因为它似乎提供了最大的灵活性,并且直接在Spring框架内工作。
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安全过滤器运行之后调用(即
注释httpfirewall
已标记应拒绝的请求。这个拦截器检测请求上的那些标志(属性),并引发一个我们的错误控制器可以处理的自定义异常。
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)))
{
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;
}
@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
{
@Override
public void addInterceptors(final InterceptorRegistry registry)
{
registry.addInterceptor(new FirewallInterceptor()).addPathPatterns("/**");
return;
}
}
错误控制器.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
{
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());
}
return "errorPage";
}
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public String handleException(final NoHandlerFoundException ex)
{
return "notFoundPage";
}
}
防火墙控制器.java
具有默认映射的控制器
NoHandlerFoundException
.
这就规避了
DispatcherServlet.noHandlerFound
,允许该方法
总是
找到一个映射以便
FirewallInterceptor.preHandle
总是被调用。这给了
RequestRejectedByFirewallException
优先于
NoHandlerFoundException异常
.
为什么这是必要的:
如前所述
here
,当
NoHandlerFoundException异常
从
DispatcherServlet
(即,当请求的URL没有对应的映射时),无法处理从上述防火墙生成的异常(
NoHandlerFoundException异常
在调用之前引发
preHandle()
),因此这些请求将进入404视图(在我的例子中,这不是期望的行为-您将看到许多“找不到带有URI的HTTP请求的映射…”消息)。这可以通过将特殊头的检查移动到
noHandlerFound
方法。不幸的是,如果不从头开始编写一个新的DispatcherServlet,就无法做到这一点,那么您也可以抛出整个Spring框架。扩展是不可能的
调度员Servlet
由于混合了受保护的、私有的和final方法,并且其属性不可访问(没有getter或setter)。也不可能包装类,因为没有可以实现的公共接口。这个类中的默认映射提供了一种绕过所有逻辑的优雅方法。
重要警告
:下面的请求映射将阻止解析静态资源,因为它优先于所有注册的资源处理程序。我仍在寻找解决方法,但有一种可能是尝试中建议的处理静态资源的方法之一
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
{
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 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);
}
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:
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: