Spring cloud gateway + JWT时Netty限制header大小导致请求bad Request问题解决
写得比较匆忙,本文主要以整体微服务架构简述开头,按照问题排查过程叙述,加以springboot启动源码浅析和解决办法。如有错误请指正先说版本Spring boot 2.0.4。Spring cloud F版 jdk1.8我们目前的微服务架构,当用户登录后会发放JWT的token令牌给前端,之后的请求都将此token放到http的header中传入后台。但是前端调用时发现之前正...
写得比较匆忙,本文主要以整体微服务架构简述开头,按照问题排查过程叙述,加以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。所以不符合需求。这就导致了这次的问题。
更多推荐
所有评论(0)