最近在Monitor生产环境时发现有些时段出现了大量的http 500异常,刚开始还以为是出现了问题,仔细排查过后发现其实是遇到了Sql注入的攻击,被Spring Security的拦截器拦截后抛出了RequestRejectedException
的异常,由于在filter层,该异常未被处理最终成了500。虽然未造成真实的影响,但是影响到了Monitor的准确性,还是要进行处理。
方案一: RequestRejectedHandler
如果是SpringBoot 2.4,有一个很方便的解决方案
@Bean
public RequestRejectedHandler requestRejectedHandler() {
// It is because of cannot catching this exception in OncePerRequestFilter.doFilterInternal in pure Tomcat Container.
// (But can catch it in Spring Boot embedded Tomcat Container.)
// In order to use RequestRejectedHandler, we need to upgrade spring security from 5.3.5.RELEASE to 5.4.0
// Refer to: https://github.com/spring-projects/spring-security/pull/7052
// Sends an error response with a configurable status code (default is 400 BAD_REQUEST)
// We can pass a different value in following constructor
return new HttpStatusRequestRejectedHandler();
}
这个Handler内部会之间返回http400,但是升级Spring风险较大,评估之后采用了如下方案:
增加Filter
Spring Security的优先级较低,在上层增加Filter对专门的Exception进行catche来处理
public class UnHandleExceptionFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(UnHandleExceptionFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
try {
chain.doFilter(servletRequest, servletResponse);
}catch (RequestRejectedException e){
logger.error("",e);
HttpServletResponse response = (HttpServletResponse) servletResponse;
ExceptionResp exceptionResp = ExceptionResp.ACL_ACCESS_IS_DENIED;
response.setStatus(exceptionResp.getStatus());
response.setContentType("application/json");
MBSExceptionResp mbsexceptionResp = new MBSExceptionResp(exceptionResp);
mbsexceptionResp.addError(e.getMessage());
String msg= JsonUtil.toStr(mbsexceptionResp);
PrintWriter writer = response.getWriter();
writer.write(msg);
}
}
@Override
public void destroy() {
}
}
该方法实现起来也很简单,本地以SpringBoot启动也不会有问题,但是我们实际上是打包成War包的形式进行部署,上到测试环境时该方案失效,并有如下异常栈:
[2021-09-01 09:16:06,740] [ERROR] 26544 [http-apr-8081-exec-266] o.s.b.w.s.s.ErrorPageFilter - ss11dd - Forwarding to error page from request [/siteservice/v1/sites/123"] due to exception [The request was rejected because the URL contained a potentially malicious String ";"] org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentially malicious String ";"
... suppressed 119 lines
at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:97) ~[spring-boot-actuator-2.3.12.RELEASE.jar:2.3.12.RELEASE]
... suppressed 16 lines
at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:126) ~[spring-boot-2.3.12.RELEASE.jar:2.3.12.RELEASE]
at org.springframework.boot.web.servlet.support.ErrorPageFilter.access$000(ErrorPageFilter.java:64) ~[spring-boot-2.3.12.RELEASE.jar:2.3.12.RELEASE]
at org.springframework.boot.web.servlet.support.ErrorPageFilter$1.doFilterInternal(ErrorPageFilter.java:101) ~[spring-boot-2.3.12.RELEASE.jar:2.3.12.RELEASE]
...
at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:119) ~[spring-boot-2.3.12.RELEASE.jar:2.3.12.RELEASE]
... suppressed 15 lines
at com.XXXXX.apiserver.common.filter.SqlInsertExceptionFilter.doFilter(SqlInsertExceptionFilter.java:32) ~[classes/:41.10.0.sqlInsertError-18608]
... suppressed 32 lines
at com.XXXXX.service.common.log.TrackingIdFilter.doFilter(TrackingIdFilter.java:36) ~[XXXXX-service-common-2.11.0-SNAPSHOT.jar:?]
... suppressed 24 lines
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:616) ~[tomcat-coyote.jar:8.5.64]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-coyote.jar:8.5.64]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:831) ~[tomcat-coyote.jar:8.5.64]
at org.apache.tomcat.util.net.AprEndpoint$SocketWithOptionsProcessor.run(AprEndpoint.java:2045) ~[tomcat-coyote.jar:8.5.64]
可以看到多了一层ErrorpageFilter
,再看这个filter的内部
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
ErrorWrapperResponse wrapped = new ErrorWrapperResponse(response);
try {
chain.doFilter(request, wrapped);
if (wrapped.hasErrorToSend()) {
handleErrorStatus(request, response, wrapped.getStatus(), wrapped.getMessage());
response.flushBuffer();
}
else if (!request.isAsyncStarted() && !response.isCommitted()) {
response.flushBuffer();
}
}
catch (Throwable ex) {
Throwable exceptionToHandle = ex;
if (ex instanceof NestedServletException) {
Throwable rootCause = ((NestedServletException) ex).getRootCause();
if (rootCause != null) {
exceptionToHandle = rootCause;
}
}
handleException(request, response, wrapped, exceptionToHandle);
response.flushBuffer();
}
}
private void handleException(HttpServletRequest request, HttpServletResponse response, ErrorWrapperResponse wrapped,
Throwable ex) throws IOException, ServletException {
Class<?> type = ex.getClass();
String errorPath = getErrorPath(type);
if (errorPath == null) {
rethrow(ex);
return;
}
if (response.isCommitted()) {
handleCommittedResponse(request, ex);
return;
}
forwardToErrorPage(errorPath, request, wrapped, ex);
}
如果遇到异常的话,会根据Exception.class,来获取对应的ErrorPage的Path,最终Forward到对应的页面,没有再向外抛出异常。
为了重新取回对异常的控制权,这里有两种方式:
1.ErrorpageFilter
有一个addErrorPages
的方法,为特定的Excpetion设置特定的Path和Controller进行处理
2.disable ErrorpageFilter
,使用自己的ExceptionHandlerFilter。
这里为了方便起见,采用了第二种方式,disable ErrorpageFilter
在SpringBoot1.0版本和2.0中是不一样的,公司使用的是2.0
public class Application extends SpringBootServletInitializer implements ApplicationContextAware {
static private ApplicationContext ctx;
public Application(){
super();
setRegisterErrorPageFilter(false);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ctx = applicationContext;
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
}
禁用之后,再对拦截器进行简单修改,拦截所有的Exception,然后针对特定的进行自己的处理