【问题记录】【SpringBoot】【Tomcat】请求头大小的限制以及配置过程原理

1  前言

上周有个项目,客户说请求报错,我看了一下 HTTP 状态码发现是 400,4XX开头的说明是客户端请求错误,常见的状态码错误可以参考我这篇:【网络】【HTTP】HTTP报文格式以及常见状态码。然后我把请求的 CURL 复制出来,贴到 Postman 里看,是个 GET 请求,请求参数也就几个,没发现特别大的,再看一下 Header 发现一个比较大的是关于权限的一个请求头,计算一下字节数在 7000 多个字节,再加上一些其他的,就超过了 8*1024(Tomcat 默认的请求头大小限制)所以会报 400,首先请求头这么大是很不合理的,设计上就有问题,就会导致每次请求都携带这么大的请求头,这不是浪费流量,浪费机器么,因为是生产环境,要先立马解决,所以我们可以暂时将限制调大,这样重新部署下 POD 发现好了,临时解决,先保证客户正常使用。那本节就来记录一下请求头的设置以及请求头最后的落点以及配置落点原理:

我们主要看 SpringBoot 以及 Servlet 容器是 Tomcat ,因为我们主要用的这两个。

2  请求头大小限制

我们看下请求头的设置,可以通过这个参数进行修改:

# 默认是 8K
server.max-http-header-size = 8196
# 比如设置 10K
server.max-http-header-size = 10240
# 也可以这么写
server.max-http-header-size = 10KB

最后是设置到 ProtocolHandler 里的 maxHttpHeaderSize,可以看到即使不设置的默认也是 8K:

然后我们看一下请求头大小的报错位置,看他是什么时候解析我们的请求,什么时候判断出来我们的请求头过长的。

这个我本地调试发现,他并不是直接去比较请求头的大小和配置的大小做比较,它是将我们的配置的请求头最大限制,放在了 inputBuffer 和 outputBuffer 里:

然后在读取的时候,通过比较 Buffer 的指针位置得出超长的报错:

3  配置落点原理

我们来看一下跟 Servlet 容器相关的配置落点实现,他们是怎么设置到我们的 Servlet 容器中的:

首先我们的参数是配置在ServerProperties 中的比如我们常见的:

@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
    // 服务端口
    private Integer port;
    // 绑定地址
    private InetAddress address;
    // 处理 HTTP 请求头中的转发信息的配置选项 NATIVE FRAMEWORK NONE
    private ForwardHeadersStrategy forwardHeadersStrategy;
    // 请求头最大限制 默认8K
    private DataSize maxHttpHeaderSize = DataSize.ofKilobytes(8);
    // 停机方式 IMMEDIATE瞬停 GRACEFUL优雅
    private Shutdown shutdown = Shutdown.IMMEDIATE;
    // HTTPS 相关
    @NestedConfigurationProperty
    private Ssl ssl;
    // 压缩相关
    @NestedConfigurationProperty
    private final Compression compression = new Compression();
    // 具体容器相关
    @NestedConfigurationProperty
    private final Http2 http2 = new Http2();
    // Servlet 相关 比如 contextPath multipart
    private final Servlet servlet = new Servlet();
    private final Tomcat tomcat = new Tomcat();
    private final Jetty jetty = new Jetty();
    private final Netty netty = new Netty();
    private final Undertow undertow = new Undertow();
}

属性值是静态的,那么它的注入时机是在哪里呢?来源于 Bean 的后置处理器:WebServerFactoryCustomizerBeanPostProcessor,后置处理器的执行时机我们看下图:

可以看到 BeanPostProcessor 是在实例化后,分别在初始化前后的处理,WebServerFactoryCustomizerBeanPostProcessor 是在 before 进行属性填充的:

/*
 * Copyright 2012-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.web.server;

import java.util.ArrayList;
import java.util.Collection;
/**
 * {@link BeanPostProcessor} that applies all {@link WebServerFactoryCustomizer} beans
 * from the bean factory to {@link WebServerFactory} beans.
 *
 * @author Dave Syer
 * @author Phillip Webb
 * @author Stephane Nicoll
 * @since 2.0.0
 */
public class WebServerFactoryCustomizerBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware {

    private ListableBeanFactory beanFactory;

    private List<WebServerFactoryCustomizer<?>> customizers;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
        Assert.isInstanceOf(ListableBeanFactory.class, beanFactory,
                "WebServerCustomizerBeanPostProcessor can only be used with a ListableBeanFactory");
        this.beanFactory = (ListableBeanFactory) beanFactory;
    }

    // 切入口
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof WebServerFactory) {
            // 属性设置
            postProcessBeforeInitialization((WebServerFactory) bean);
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @SuppressWarnings("unchecked")
    private void postProcessBeforeInitialization(WebServerFactory webServerFactory) {
        // 获取所有的自定义器 getCustomizers
        LambdaSafe.callbacks(WebServerFactoryCustomizer.class, getCustomizers(), webServerFactory)
                .withLogger(WebServerFactoryCustomizerBeanPostProcessor.class)
                // 循环设置
                .invoke((customizer) -> customizer.customize(webServerFactory));
    }

    private Collection<WebServerFactoryCustomizer<?>> getCustomizers() {
        if (this.customizers == null) {
            // Look up does not include the parent context
            // 获取到上下文中所有的 WebServerFactoryCustomizer 类型的 Bean
            this.customizers = new ArrayList<>(getWebServerFactoryCustomizerBeans());
            // 排序
            this.customizers.sort(AnnotationAwareOrderComparator.INSTANCE);
            this.customizers = Collections.unmodifiableList(this.customizers);
        }
        return this.customizers;
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    private Collection<WebServerFactoryCustomizer<?>> getWebServerFactoryCustomizerBeans() {
        return (Collection) this.beanFactory.getBeansOfType(WebServerFactoryCustomizer.class, false, false).values();
    }

}

我们看到这里的属性设置时通过 Bean 后置处理器设置的,并且默认会有 5个 自定义器进行逐个设置,最后都是设置给了 WebServerFactory,而默认的 Servlet 容器是 Tomcat ,所以这里工厂其实是TomcatServletWebServerFactory:

TomcatServletWebServerFactory 是怎么来的,它是来自于 ServletWebServerFactoryConfiguration 配置类,而 ServletWebServerFactoryConfiguration 配置类来源于自动装配类:ServletWebServerFactoryAutoConfiguration 如下

@Configuration(proxyBeanMethods = false)
class ServletWebServerFactoryConfiguration {

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
    @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
    static class EmbeddedTomcat {

        @Bean
        TomcatServletWebServerFactory tomcatServletWebServerFactory(
                ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,
                ObjectProvider<TomcatContextCustomizer> contextCustomizers,
                ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
            TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
            factory.getTomcatConnectorCustomizers()
                    .addAll(connectorCustomizers.orderedStream().collect(Collectors.toList()));
            factory.getTomcatContextCustomizers()
                    .addAll(contextCustomizers.orderedStream().collect(Collectors.toList()));
            factory.getTomcatProtocolHandlerCustomizers()
                    .addAll(protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));
            return factory;
        }

    }
    ...
}
// ServletWebServerFactoryConfiguration 配置类又来来源于 ServletWebServerFactoryAutoConfiguration 自动装配类
@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
        ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
        ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
        ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {
    ...
}

ServletWebServerFactoryAutoConfiguration 自动装配类,来源于自动装配包下的 spring.factories

那我们继续看自定义器里的设置,比如请求头大小的限制,是放到 TomcatServletWebServerFactory 的集合里:

然后在 TomcatServletWebServerFactory 创建 webServer 的时候,拿出上边设置的自定义配置,进行遍历设置:

我们这里画个图整体梳理一下以及上边的 5个自定义器的由来:

4  小结

好啦,本节主要看了下请求头大小的限制配置以及服务的常见配置参数的配置过程,主要是通过自动装配引入 Tomcat 的 TomcatServletWebServerFactory (用于创建Tomcat 的工厂)然后通过 WebServerFactoryCustomizerBeanPostProcessor Bean 的后置处理器对工厂进行配置的赋值,然后工厂在创建 Tomcat 的时候,将配置值设置进 Tomcat,有理解不对的地方还请指正哈。 

posted @ 2024-11-06 21:54  酷酷-  阅读(472)  评论(0编辑  收藏  举报