文章目录
HttpFirewall
HttpFirewall
是spring security提供的HTTP防火墙,它可以用于拒绝潜在的危险请求或者包装这些请求进而控制其行为。HttpFirewall
被注入到FilterChainProxy
中,并在spring security过滤器链执行之前被触发。
8.1HttpFirewall
简介
Spring security中通过
HttpFirewall
来检查请求路径以及参数是否合法,如果合法,才会进入到过滤器链中进行处理。
public interface HttpFirewall {
// 对请求对象进行检验并封装
FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException;
// 对响应对象进行封装
HttpServletResponse getFirewalledResponse(HttpServletResponse response);
}
FirewalledRequest
是封装后的请求类,但实际上该类只是在HttpServletRequestWrapper
的基础上增加了reset
方法。当spring security过滤器链执行完毕时,由FilterChainProxy
负责调用该方法,以便重置全部或者部分属性。
FirewalledResponse
是封装后的响应类,该类主要重写了sendRedirect
、setHeader
、addHeader
以及addCookie
四个方法,在每一个方法中都对其参数进行校验,以确保参数中不含有\r
和\n
。
HttpFirewall
一共有两个实现类:
DefaultHttpFirewall
:虽然名字中包含default,但这并不是框架默认使用的HTTP防火墙,它只是一个检查相对宽松的防火墙。StrictHttpFirewall
:这是一个检查严格的HTTP防火墙,默认即此。
HttpFirewall
中对请求的合法性校验在FilterChainProxy#doFilterInternal
方法中触发。
需要注意的是
HttpFirewall
的配置位置,在spring security框架中有两个地方涉及了HttpFirewall
实例的获取:
- 在
FilterChainProxy
属性定义中,默认创建的HttpFirewall
实例就是StrictHttpFirewall
。FilterChainProxy
是在WebSecurity#performBuild
方法中构建的,而WebSecurity
实现了ApplicationContextAware
接口,并实现了接口中的setApplicationContext
方法,在该方法中,从spring容器中查找到HttpFirewall
对并赋值给httpFirewall
属性。最终在performBuild
方法中,将FilterChainProxy
对象构建成功后,如果httpFirewall
不为空,就把httpFirewall
配置给FilterChainProxy
对象。因此,如果spring容器中存在
HttpFirewall
实例,则最终使用spring容器提供的实例;如果不存在,则使用FilterChainProxy
中默认定义的StrictHttpFirewall
。
8.2HttpFirewall
严格模式
在
FilterChainProxy#doFilterInternal
中触发请求校验的方法如下:
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 请求的校验主要是在getFirewalledRequest方法中完成的。在进入spring security过滤器链之前,请求对象和响应对象
// 都分别换成FirewalledRequest和FirewalledResponse了
FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
List<Filter> filters = getFilters(firewallRequest);
// 省略其他
virtualFilterChain.doFilter(firewallRequest, firewallResponse);
}
需要注意的是,无论是
FirewalledRequest
还是FirewalledResponse
,在经过spring security过滤器链的时候,还会通过装饰器模式增强其功能,所以开发者最终在接口中拿到的HttpServletRequest
和HttpServletResponse
对象,并不是这里的FirewalledRequest
和FirewalledResponse
。
重点分析getFirewalledRequest
方法:
// StrictHttpFirewall
@Override
public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
// 校验请求方法是否合法
rejectForbiddenHttpMethod(request);
// 校验请求中的非法字符
rejectedBlocklistedUrls(request);
// 校验主机信息
rejectedUntrustedHosts(request);
// 判断参数格式是否合法
if (!isNormalized(request)) {
throw new RequestRejectedException("The request was rejected because the URL was not normalized.");
}
String requestUri = request.getRequestURI();
// 判断请求字符是否合法
if (!containsOnlyPrintableAsciiCharacters(requestUri)) {
throw new RequestRejectedException(
"The requestURI was rejected because it can only contain printable ASCII characters.");
}
return new StrictFirewalledRequest(request);
}
接下来会逐一分析这五个校验方法。
8.2.1rejectForbiddenHttpMethod
主要用来判断请求方法是否合法:
private void rejectForbiddenHttpMethod(HttpServletRequest request) {
if (this.allowedHttpMethods == ALLOW_ANY_HTTP_METHOD) {
return;
}
if (!this.allowedHttpMethods.contains(request.getMethod())) {
throw new RequestRejectedException(
"The request was rejected because the HTTP method \"" + request.getMethod()
+ "\" was not included within the list of allowed HTTP methods " + this.allowedHttpMethods);
}
}
allowedHttpMethods
是一个Set
集合,默认情况下该集合中包含七个常见的方法:DELETE
、GET
、HEAD
、OPTIONS
、PATCH
、POST
、PUT
,ALLOW_ANY_HTTP_METHOD
变量默认情况下则是一个空的Set
集合。
开发者可以根据实际需求修改allowedHttpMethods
变量的值,进而调整允许的请求方法。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 第一种方式:通过设置allowedHttpMethods进行修改,此时只允许POST请求。
*/
// @Bean
// HttpFirewall httpFirewall() {
// StrictHttpFirewall strictHttpFirewall = new StrictHttpFirewall();
// Set<String> allowedHttpMethods = new HashSet<>();
// allowedHttpMethods.add(HttpMethod.POST.name());
// strictHttpFirewall.setAllowedHttpMethods(allowedHttpMethods);
// return strictHttpFirewall;
// }
/**
* 第二种方式:设置参数为true,让allowedHttpMethods等于ALLOW_ANY_HTTP_METHOD,进而允许所有请求通过。
*/
@Bean
HttpFirewall httpFirewall() {
StrictHttpFirewall strictHttpFirewall = new StrictHttpFirewall();
strictHttpFirewall.setUnsafeAllowAnyHttpMethod(true);
return strictHttpFirewall;
}
// 省略其他
}
8.2.2rejectedBlocklistedUrls
主要用来校验请求URL是否规范:
- 如果请求URL地址中在编码之前或者之后,包含了分号,即
;
、%3b
、%3B
,则该请求会被拒绝,可以通过setAllowSemicolon
方法开启或者关闭这一规则。- 如果请求URL地址中在编码之前或者之后,包含了斜杠,即
%2f
、%2F
,则该请求会被拒绝,可以通过setAllowUrlEncodedSlash
方法开启或者关闭这一规则。- 如果请求URL地址中在编码之前或者之后,包含了反斜杠,即
\\
、%5c
、%5C
,则该请求会被拒绝,可以通过setAllowBackSlash
方法开启或者关闭这一规则。- 如果请求URL地址中在编码之后包含了
%25
或者在编码之前包含了%
,则该请求会被拒绝,可以通过setAllowUrlEncodedPercent
方法开启或者关闭这一规则。- 如果请求URL在编码后包含了英文句号
%2e
或者%2E
,则该请求会被拒绝,可以通过setAllowUrlEncodedPeriod
方法开启或者关闭这一规则。
private void rejectedBlocklistedUrls(HttpServletRequest request) {
// 校验编码后的请求地址
for (String forbidden : this.encodedUrlBlocklist) {
// 主要是校验了contextPath和requestURI两个属性,这两个属性是客户端传递来的字符串,未做任何更改
if (encodedUrlContains(request, forbidden)) {
throw new RequestRejectedException(
"The request was rejected because the URL contained a potentially malicious String \""
+ forbidden + "\"");
}
}
// 校验解码后的请求地址
for (String forbidden : this.decodedUrlBlocklist) {
// 主要校验了servletPath、pathInfo两个属性,需要注意的是,这个是经过解码后的
if (decodedUrlContains(request, forbidden)) {
throw new RequestRejectedException(
"The request was rejected because the URL contained a potentially malicious String \""
+ forbidden + "\"");
}
}
}
8.2.3rejectedUntrustedHosts
主要用来校验host是否受信任:
private void rejectedUntrustedHosts(HttpServletRequest request) {
String serverName = request.getServerName();
if (serverName != null && !this.allowedHostnames.test(serverName)) {
throw new RequestRejectedException(
"The request was rejected because the domain " + serverName + " is untrusted.");
}
}
allowedHostnames
默认总是返回true
,即默认信任所有的host,可以根据实际需求对此进行配置:
@Bean
HttpFirewall httpFirewall() {
StrictHttpFirewall strictHttpFirewall = new StrictHttpFirewall();
strictHttpFirewall.setAllowedHostnames((hostname) -> hostname.equalsIgnoreCase("local.javaboy.org"));
return strictHttpFirewall;
}
8.2.4isNormalized
主要用来检查请求地址是否规范,即不包含"./"、"/…/“以及”/."三种字符。
private static boolean isNormalized(HttpServletRequest request) {
// 对requestURI、contextPath、servletPath以及pathInfo分别进行了校验
if (!isNormalized(request.getRequestURI())) {
return false;
}
if (!isNormalized(request.getContextPath())) {
return false;
}
if (!isNormalized(request.getServletPath())) {
return false;
}
if (!isNormalized(request.getPathInfo())) {
return false;
}
return true;
}
8.2.5containsOnlyPrintableAsciiCharacters
用来校验请求地址中是否包含不可打印的ASCII字符。
private static boolean containsOnlyPrintableAsciiCharacters(String uri) {
int length = uri.length();
for (int i = 0; i < length; i++) {
char ch = uri.charAt(i);
if (ch < '\u0020' || ch > '\u007e') {
return false;
}
}
return true;
}
StrictHttpFirewall
中的校验规则,前三种可以通过相关方法调整,后面两种不可调整。
8.3HttpFirewall
普通模式
HttpFirewall
普通模式就是使用DefaultHttpFirewall
,该类的校验规则就要简单很多:
// DefaultHttpFirewall
@Override
public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
// 构建RequestWrapper对象对原始请求中的功能进行了增强,例如将请求地址中的//格式化为/,以及将请求中的servletPath和
// pathInfo中用分号隔开的参数提取出来,只保留路径即可
FirewalledRequest firewalledRequest = new RequestWrapper(request);
if (!isNormalized(firewalledRequest.getServletPath()) || !isNormalized(firewalledRequest.getPathInfo())) {
throw new RequestRejectedException(
"Un-normalized paths are not supported: " + firewalledRequest.getServletPath()
+ ((firewalledRequest.getPathInfo() != null) ? firewalledRequest.getPathInfo() : ""));
}
String requestURI = firewalledRequest.getRequestURI();
// 判断requestURI中是否包含编码后的斜杠
if (containsInvalidUrlEncodedSlash(requestURI)) {
throw new RequestRejectedException("The requestURI cannot contain encoded slash. Got " + requestURI);
}
return firewalledRequest;
}
一般来说,并不建议在项目中使用
DefaultHttpFirewall
,如果一定要用,只需要提供一个实例即可:
@Bean
HttpFirewall httpFirewall() {
return new DefaultHttpFirewall();
}