前言
这一章没有干货, 可以不看, 但我不能不讲
说白了, 就是spring security 内部有一个防火墙保护服务器的安全, 该防火墙有两种模式, 严格模式和普通模式
spring security默认是严格模式
知道这些就可以了
HttpFirewall介绍
是什么?
一个Spring Security提供的 HTTP 防火墙
在这个防火墙中 Spring Security 帮我们防住了很多危险请求
而HttpFirewall接口便用于处理request和response请求的
现在我们分析下这个接口的两个方法
public interface HttpFirewall {
FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException;
HttpServletResponse getFirewalledResponse(HttpServletResponse response);
}
getFirewalledRequest这个方法接收了我们的请求, 通过某种方法, 将我们的请求处理成FirewalledRequest对象, 如果失败抛出了一个RequestRejectedException拒绝请求异常
而getFirewalledResponse这个方法接受了我们的响应, 将其包装下, 最后返回相同的 HttpServletResponse给我们的客户
这个接口有两个实现类
默认是严格模式的HTTP防火墙
我们还可以定义Spring Bean:
还可以接着WebSecurity的方法配置HttpFirewall:
流程分析
HttpFirewall的执行流程非常简单
HttpFirewall严格模式
上面的流程将整个严格模式HttpFirewall算是研究的差不多了
这里再强调下他有哪些功能
rejectForbiddenHttpMethod: 校验请求方法是否合法rejectedBlocklistedUrls: 校验请求中的非法字符rejectedUntrustedHosts: 校验主机信息(这里默认信任所有Host)isNormalized: 判断参数格式是否合法rejectNonPrintableAsciiCharactersInFieldName: 判断请求字符是否合法
HttpFirewall普通模式
普通模式的类名叫DefaultHttpFirewall
在源码中, 首先
FirewalledRequest firewalledRequest = new RequestWrapper(request);
构造了一个 RequestWrapper 对象, 在这个过程中做了
- 将请求地址中的
//格式化为/ - 将请求中的
servletPath和pathInfo中的分号隔开的参数提取出来, 只保留路径即可
if (!isNormalized(firewalledRequest.getServletPath()) || !isNormalized(firewalledRequest.getPathInfo())) {
通过isNormalized来判断ServletPath和PathInfo是否符合标准
if (containsInvalidUrlEncodedSlash(requestURI)) {
判断requestURI中是否包含编码后的斜杠, 即%2f或%2F, 默认是不允许存在编码后的斜杠的, 如果开发者有需求, 则取修改allowUrlEncodedSlash = true即可
默认不建议在项目中使用
DefaultHttpFirewall方案, 不安全, 如果开发者需要使用, 则默认new一个出来当作Spring Bean就行了
更多信息可以查看: spring-security使用-安全防护HttpFirewall(七) - 意犹未尽 - 博客园 (cnblogs.com)
详细情况
必须是标准化 URL
请求地址必须是标准化 URL。
什么是标准化 URL?标准化 URL 主要从四个方面来判断,我们来看下源码:
StrictHttpFirewall#isNormalized:
private static boolean isNormalized(HttpServletRequest request) {
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;
}
getRequestURI 就是获取请求协议之外的字符;getContextPath 是获取上下文路径,相当于是 project 的名字;getServletPath 这个就是请求的 servlet 路径,getPathInfo 则是除过 contextPath 和 servletPath 之后剩余的部分。
这四种路径中,都不能包含如下字符串:
"./", "/../" or "/."
限制请求方法
public class StrictHttpFirewall implements HttpFirewall {
//空的集合
private static final Set<String> ALLOW_ANY_HTTP_METHOD = Collections.unmodifiableSet(Collections.emptySet());
private Set<String> allowedHttpMethods = createDefaultAllowedHttpMethods();
//设置允许的请求方式
private static Set<String> createDefaultAllowedHttpMethods() {
Set<String> result = new HashSet();
result.add(HttpMethod.DELETE.name());
result.add(HttpMethod.GET.name());
result.add(HttpMethod.HEAD.name());
result.add(HttpMethod.OPTIONS.name());
result.add(HttpMethod.PATCH.name());
result.add(HttpMethod.POST.name());
result.add(HttpMethod.PUT.name());
return result;
}
public void setUnsafeAllowAnyHttpMethod(boolean unsafeAllowAnyHttpMethod) {
//如果是false则设置空的集合
this.allowedHttpMethods = unsafeAllowAnyHttpMethod ? ALLOW_ANY_HTTP_METHOD : createDefaultAllowedHttpMethods();
}
private void rejectForbiddenHttpMethod(HttpServletRequest request) {
if (this.allowedHttpMethods != ALLOW_ANY_HTTP_METHOD) {
//如果不存在运行的方法里面 则抛出异常
if (!this.allowedHttpMethods.contains(request.getMethod())) {
throw new RequestRejectedException("The request was rejected because the HTTP method \"" + request.getMethod() + "\" was not included within the whitelist " + this.allowedHttpMethods);
}
}
}
}
使用方式
@Bean
HttpFirewall httpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setUnsafeAllowAnyHttpMethod(true);
return firewall;
}
请求地址不能有分号
如地址:http://localhost:8080/index;id=ddd
public class StrictHttpFirewall implements HttpFirewall {
private Set<String> encodedUrlBlacklist = new HashSet();
private Set<String> decodedUrlBlacklist = new HashSet();
//;的urlecod和decode的几种
private static final List<String> FORBIDDEN_SEMICOLON = Collections.unmodifiableList(Arrays.asList(";", "%3b", "%3B"));
public void setAllowSemicolon(boolean allowSemicolon) {
//如果是false则删除调
if (allowSemicolon) {
this.urlBlacklistsRemoveAll(FORBIDDEN_SEMICOLON);
} else {
this.urlBlacklistsAddAll(FORBIDDEN_SEMICOLON);
}
}
//加入到校验list
private void urlBlacklistsAddAll(Collection<String> values) {
this.encodedUrlBlacklist.addAll(values);
this.decodedUrlBlacklist.addAll(values);
}
//清除到校验list
private void urlBlacklistsRemoveAll(Collection<String> values) {
this.encodedUrlBlacklist.removeAll(values);
this.decodedUrlBlacklist.removeAll(values);
}
private void rejectedBlacklistedUrls(HttpServletRequest request) {
Iterator var2 = this.encodedUrlBlacklist.iterator();
String forbidden;
do {
if (!var2.hasNext()) {
var2 = this.decodedUrlBlacklist.iterator();
do {
if (!var2.hasNext()) {
return;
}
forbidden = (String)var2.next();
} while(!decodedUrlContains(request, forbidden));
throw new RequestRejectedException("The request was rejected because the URL contained a potentially malicious String \"" + forbidden + "\"");
}
forbidden = (String)var2.next();
} while(!encodedUrlContains(request, forbidden));
throw new RequestRejectedException("The request was rejected because the URL contained a potentially malicious String \"" + forbidden + "\"");
}
}
-
如果请求
URL(无论是URL编码前还是URL编码后)包含了分号(;或者%3b或者%3B)则该请求会被拒绝。通过开关函数
setAllowSemicolon(boolean)可以设置是否关闭该规则。缺省使用该规则。
必须是可打印的 ASCII 字符
private static boolean containsOnlyPrintableAsciiCharacters(String uri) {
int length = uri.length();
for (int i = 0; i < length; i++) {
char c = uri.charAt(i);
if (c < '\u0020' || c > '\u007e') {
return false;
}
}
return true;
}
不能使用双斜杠
@Bean
HttpFirewall httpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowUrlEncodedDoubleSlash(true);
return firewall;
}
% 不被允许
如果需要去掉
@Bean
HttpFirewall httpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall(); //允许%的配置
firewall.setAllowUrlEncodedPercent(true);
return firewall;
}
-
如果请求
URL在URL编码后包含了%25(URL编码了的百分号%),或者在URL编码前包含了百分号%则该请求会被拒绝。通过开关函数
setAllowUrlEncodedPercent(boolean)可以设置是否关闭该规则。缺省使用该规则。
反斜杠不被允许
如果需要去掉
@Bean
HttpFirewall httpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowBackSlash(true);
firewall.setAllowUrlEncodedSlash(true);
return firewall;
}
-
如果请求
URL(无论是URL编码前还是URL编码后)包含了斜杠(%2f或者%2F)则该请求会被拒绝。通过开关函数
setAllowUrlEncodedSlash(boolean)可以设置是否关闭该规则。缺省使用该规则。 -
如果请求
URL(无论是URL编码前还是URL编码后)包含了反斜杠(\或者%5c或者%5B)则该请求会被拒绝。通过开关函数
setAllowBackSlash(boolean)可以设置是否关闭该规则。缺省使用该规则。
. 不被允许
@Bean
HttpFirewall httpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowUrlEncodedPeriod(true);
return firewall;
}
-
如果请求
URL在URL编码后包含了URL编码的英文句号.(%2e或者%2E)则该请求会被拒绝。通过开关函数
setAllowUrlEncodedPeriod(boolean)可以设置是否关闭该规则。缺省使用该规则。
注意:这里提到的"
URL编码后"对应英文是URL encoded,"URL编码前"指的是未执行URL编码的原始URL字符串,或者是"URL编码后"的URL经过解码URL decode得到的URL字符串(应该等于原始URL字符串)。