8.Spring security中的HttpFirewall

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是封装后的响应类,该类主要重写了sendRedirectsetHeaderaddHeader以及addCookie四个方法,在每一个方法中都对其参数进行校验,以确保参数中不含有\r\n
HttpFirewall一共有两个实现类:

在这里插入图片描述

  • DefaultHttpFirewall:虽然名字中包含default,但这并不是框架默认使用的HTTP防火墙,它只是一个检查相对宽松的防火墙。
  • StrictHttpFirewall:这是一个检查严格的HTTP防火墙,默认即此。

HttpFirewall中对请求的合法性校验在FilterChainProxy#doFilterInternal方法中触发。

需要注意的是HttpFirewall的配置位置,在spring security框架中有两个地方涉及了HttpFirewall实例的获取:

  1. FilterChainProxy属性定义中,默认创建的HttpFirewall实例就是StrictHttpFirewall
  2. 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过滤器链的时候,还会通过装饰器模式增强其功能,所以开发者最终在接口中拿到的HttpServletRequestHttpServletResponse对象,并不是这里的FirewalledRequestFirewalledResponse
重点分析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集合,默认情况下该集合中包含七个常见的方法:DELETEGETHEADOPTIONSPATCHPOSTPUTALLOW_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是否规范:

  1. 如果请求URL地址中在编码之前或者之后,包含了分号,即;%3b%3B,则该请求会被拒绝,可以通过setAllowSemicolon方法开启或者关闭这一规则。
  2. 如果请求URL地址中在编码之前或者之后,包含了斜杠,即%2f%2F,则该请求会被拒绝,可以通过setAllowUrlEncodedSlash方法开启或者关闭这一规则。
  3. 如果请求URL地址中在编码之前或者之后,包含了反斜杠,即\\%5c%5C,则该请求会被拒绝,可以通过setAllowBackSlash方法开启或者关闭这一规则。
  4. 如果请求URL地址中在编码之后包含了%25或者在编码之前包含了%,则该请求会被拒绝,可以通过setAllowUrlEncodedPercent方法开启或者关闭这一规则。
  5. 如果请求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();
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
你需要在代码定义 JwtConfigurer 类,或者使用 Spring Security 提供的 JwtConfigurer 类,具体代码如下: ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtTokenProvider jwtTokenProvider; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .addFilterBefore(new JwtTokenFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class) .authorizeRequests() .antMatchers("/api/**").authenticated() .anyRequest().permitAll(); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**"); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(12); } } ``` 其,JwtTokenFilter 是实现了 JWT 鉴权的过滤器。通过在 `configure` 方法添加 `.addFilterBefore(new JwtTokenFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class)`,让该过滤器先于 Spring Security 的默认过滤器执行,以实现 JWT 鉴权。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值