Spring cloud gateway + JWT時Netty限制header大小導致請求bad Request問題解決

寫得比較匆忙,本文主要以整體微服務架構簡述開頭,按照問題排查過程敘述,加以springboot啓動源碼淺析和解決辦法。

如有錯誤請指正

先說版本Spring boot 2.0.4。Spring cloud F版 jdk1.8

 

我們目前的微服務架構,當用戶登錄後會發放JWT的token令牌給前端,之後的請求都將此token放到http的header中傳入後臺。

但是前端調用時發現之前正常的請求報錯400 bad request。查找資料說是請求頭過長導致。

 

首先排查權限系統默認的tomcat的header請求,發現默認大小不夠。配置:

server.tomcat.maxhttpheadersize==102400

未解決。

 

聯想到我們微服務商是通過gateway網關轉發的。準備去調整gateway的server.tomcat.maxhttpheadersize。但是gateway是由webflux開發的使用netty服務器的。沒有這個配置項。

 

查看資料發現存在另外一個配置server.maxhttpheadersize同樣可以設置header大小。

於是配置server.maxhttpheadersize==102400 啓動,但是發現並不生效。

於是決定debug啓動過程,發現ServerProperties中讀取到了配置102400,但是不生效。

再深入 看netty啓動過程。發現nettry並沒有配置使用ServerProperties配置類。

 

分析啓動過程,

springboot項目啓動,調用
SpringApplication.run(Application.class, args);

內部調用構造器

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
		this.resourceLoader = resourceLoader;
		Assert.notNull(primarySources, "PrimarySources must not be null");
		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
		this.webApplicationType = deduceWebApplicationType();
		setInitializers((Collection) getSpringFactoriesInstances(
				ApplicationContextInitializer.class));
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		this.mainApplicationClass = deduceMainApplicationClass();
	}

這裏通過deduceWebApplicationType();給webApplicaitonType賦值

	private WebApplicationType deduceWebApplicationType() {
		if (ClassUtils.isPresent(REACTIVE_WEB_ENVIRONMENT_CLASS, null)
				&& !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null)
				&& !ClassUtils.isPresent(JERSEY_WEB_ENVIRONMENT_CLASS, null)) {
			return WebApplicationType.REACTIVE;
		}
		for (String className : WEB_ENVIRONMENT_CLASSES) {
			if (!ClassUtils.isPresent(className, null)) {
				return WebApplicationType.NONE;
			}
		}
		return WebApplicationType.SERVLET;
	}

這裏根據存在的類判斷是哪種模式,如果是

org.springframework.web.reactive.DispatcherHandler

並且不存在

org.glassfish.jersey.server.ResourceConfi
org.springframework.web.servlet.DispatcherServlet

返回

WebApplicationType.REACTIVE

再往下看到run方法內調用createApplicationContext

context = createApplicationContext();

 

	protected ConfigurableApplicationContext createApplicationContext() {
		Class<?> contextClass = this.applicationContextClass;
		if (contextClass == null) {
			try {
				switch (this.webApplicationType) {
				case SERVLET:
					contextClass = Class.forName(DEFAULT_WEB_CONTEXT_CLASS);
					break;
				case REACTIVE:
					contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
					break;
				default:
					contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
				}
			}
			catch (ClassNotFoundException ex) {
				throw new IllegalStateException(
						"Unable create a default ApplicationContext, "
								+ "please specify an ApplicationContextClass",
						ex);
			}
		}
		return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
	}

這裏根據前面得到的REACTIVE服之context爲

	public static final String DEFAULT_REACTIVE_WEB_CONTEXT_CLASS = "org.springframework."
			+ "boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext";

繼承於ReactiveWebServerApplicationContext類。調用createWebServer方法給webServer賦值。

	private void createWebServer() {
		WebServer localServer = this.webServer;
		if (localServer == null) {
			this.webServer = getWebServerFactory().getWebServer(getHttpHandler());
		}
		initPropertySources();
	}

因爲我們這裏啓用的netty,所以調用到NettyReactiveWebServerFactory工廠類,而此類中通過

private HttpServer createHttpServer() {
		return HttpServer.builder().options((options) -> {
			options.listenAddress(getListenAddress());
			if (getSsl() != null && getSsl().isEnabled()) {
				SslServerCustomizer sslServerCustomizer = new SslServerCustomizer(
						getSsl(), getSslStoreProvider());
				sslServerCustomizer.customize(options);
			}
			if (getCompression() != null && getCompression().getEnabled()) {
				CompressionCustomizer compressionCustomizer = new CompressionCustomizer(
						getCompression());
				compressionCustomizer.customize(options);
			}
			applyCustomizers(options);
		}).build();
	}

生成httpServer。而在HttpServer總通過builder構建者的options.可以配置請求大小。

	public static final class Builder extends ServerOptions.Builder<Builder> {

		public static final int DEFAULT_MAX_INITIAL_LINE_LENGTH = 4096;
		public static final int DEFAULT_MAX_HEADER_SIZE         = 8192;
		public static final int DEFAULT_MAX_CHUNK_SIZE          = 8192;
		public static final boolean DEFAULT_VALIDATE_HEADERS    = true;
		public static final int DEFAULT_INITIAL_BUFFER_SIZE     = 128;

		private BiPredicate<HttpServerRequest, HttpServerResponse> compressionPredicate;

		private int minCompressionResponseSize = -1;
		private int maxInitialLineLength = DEFAULT_MAX_INITIAL_LINE_LENGTH;
		private int maxHeaderSize = DEFAULT_MAX_HEADER_SIZE;
		private int maxChunkSize = DEFAULT_MAX_CHUNK_SIZE;
		private boolean validateHeaders = DEFAULT_VALIDATE_HEADERS;
		private int initialBufferSize = DEFAULT_INITIAL_BUFFER_SIZE;
		public final Builder maxHeaderSize(int value) {
			if (value <= 0) {
				throw new IllegalArgumentException("maxHeaderSize must be strictly positive");
			}
			this.maxHeaderSize = value;
			return get();
		}

這裏HttpServer是final類和不變類,通過構建者聲稱的HttpServer無法被修改。

所以這裏我們想要自己修改這個值,只能將整個NettyReactiveWebServerFactory工廠類在他自動注入前注入,然後運行到autoconfiguration時,就不會再此注入原有的NettyReactiveWebServerFactory類了。

這裏放出重寫的代碼

    private HttpServer createHttpServer() {
        return HttpServer.builder().options((options) -> {
            options.maxHeaderSize(102400);
            options.listenAddress(getListenAddress());
            if (getSsl() != null && getSsl().isEnabled()) {
                SslServerCustomizer sslServerCustomizer = new SslServerCustomizer(
                        getSsl(), getSslStoreProvider());
                sslServerCustomizer.customize(options);
            }
            if (getCompression() != null && getCompression().getEnabled()) {
                CompressionCustomizer compressionCustomizer = new CompressionCustomizer(
                        getCompression());
                compressionCustomizer.customize(options);
            }
            applyCustomizers(options);
        }).build();
    }
    @Bean
    @Order(-1)
    public NettyReactiveWebServerFactory nettyReactiveWebServerFactory() {
        return new NettyReactiveWebServerFactory();
    }

啓動調試,發現102400已經被配置到了netty中。

 

 

 

本地調試啓動權限系統,網關,前端調用。過了。

到這裏解決了spring cloud gateway中netty自定義header請求限制大小問題。

但是,部署到dev環境,請求,失敗。。。

打印日誌調試,發現權限系統和網關配置的都沒問題。

而且報錯請求根本沒進入gateway。

這就又想到我們在k8s集羣內,有個ingress在前面,又去配置ingress-controller的配置項。配置

client-header-buffer-size == 100k

重啓pod。 調用,成功。

 

其實前面還有一層slb,4層負載,只不過4層負載不到http的應用層,所以沒有配置大小;到ingress是http服務器,雖然協議沒規定一定要限制大小;但是ingress的默認nginx配置了16k。所以不符合需求。這就導致了這次的問題。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章