zoukankan      html  css  js  c++  java
  • 记一次升级Tomcat

    总述

        JDK都要出12了,而我们项目使用的jdk却仍然还停留在JDK1.6。为了追寻技术的发展的脚步,我这边准备将项目升级到JDK1.8。而作为一个web项目,我们的容器使用的是Tomcat。看了下Tomcat版本与JDK版本之间的兼容关系http://tomcat.apache.org/whichversion.html以及网上所传的各种JDK1.8和Tomcat7不兼容的问题, 我决定将Tomcat升级到8。我这里本地验证采用的tomcat版本是8.5.38https://tomcat.apache.org/download-80.cgi

    问题一:请求js文件报404错误

        其实这个问题严格来讲不是升级到Tomcat8出现的问题,而是升级到Tomcat9出现的问题。正好我开始尝试的是Tomcat9,无法解决这个问题才降到Tomcat8。所以这里一并记录下来。

        这个问题在从Tomcat6升级到Tomcat7之后也会存在,原因如下,在项目代码中对js的请求路径中包含了{、}等特殊符号:

    <script type="text/javascript" src="${ctx}/js/common/include_css.js?{'ctx':'${ctx}','easyui':'easyui'}"></script>
    
    

        前台会发现加载js的时候报了404的错误,后台报错信息如下:

    Invalid character found in the request target.The valid characters are defined in RFC 7230 and RFC3986
    

        出现这个问题的原因是因为Tomcat升级之后对安全进行了升级,其中就有对请求中的特殊字符进行校验,具体校验规则参照下面的代码:

    (InternalInputBuffer、InternalAprInputBuffer、InternalNioInputBuffer)

    /**
     * Read the request line. This function is meant to be used during the
     * HTTP request header parsing. Do NOT attempt to read the request body
     * using it.
     *
     * @throws IOException If an exception occurs during the underlying socket
     * read operations, or if the given buffer is not big enough to accommodate
     * the whole line.
     */
    @Override
    public boolean parseRequestLine(boolean useAvailableDataOnly)
    
    	throws IOException {
    
    	int start = 0;
    
    	//
    	// Skipping blank lines
    	//
    
    	byte chr = 0;
    	do {
    
    		// Read new bytes if needed
    		if (pos >= lastValid) {
    			if (!fill())
    				throw new EOFException(sm.getString("iib.eof.error"));
    		}
    		// Set the start time once we start reading data (even if it is
    		// just skipping blank lines)
    		if (request.getStartTime() < 0) {
    			request.setStartTime(System.currentTimeMillis());
    		}
    		chr = buf[pos++];
    	} while ((chr == Constants.CR) || (chr == Constants.LF));
    
    	pos--;
    
    	// Mark the current buffer position
    	start = pos;
    
    	//
    	// Reading the method name
    	// Method name is a token
    	//
    
    	boolean space = false;
    
    	while (!space) {
    
    		// Read new bytes if needed
    		if (pos >= lastValid) {
    			if (!fill())
    				throw new EOFException(sm.getString("iib.eof.error"));
    		}
    
    		// Spec says method name is a token followed by a single SP but
    		// also be tolerant of multiple SP and/or HT.
    		if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
    			space = true;
    			request.method().setBytes(buf, start, pos - start);
    		} else if (!HttpParser.isToken(buf[pos])) {
    			throw new IllegalArgumentException(sm.getString("iib.invalidmethod"));
    		}
    
    		pos++;
    
    	}
    
    	// Spec says single SP but also be tolerant of multiple SP and/or HT
    	while (space) {
    		// Read new bytes if needed
    		if (pos >= lastValid) {
    			if (!fill())
    				throw new EOFException(sm.getString("iib.eof.error"));
    		}
    		if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
    			pos++;
    		} else {
    			space = false;
    		}
    	}
    
    	// Mark the current buffer position
    	start = pos;
    	int end = 0;
    	int questionPos = -1;
    
    	//
    	// Reading the URI
    	//
    
    	boolean eol = false;
    
    	while (!space) {
    
    		// Read new bytes if needed
    		if (pos >= lastValid) {
    			if (!fill())
    				throw new EOFException(sm.getString("iib.eof.error"));
    		}
    
    		// Spec says single SP but it also says be tolerant of HT
    		if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
    			space = true;
    			end = pos;
    		} else if ((buf[pos] == Constants.CR)
    				   || (buf[pos] == Constants.LF)) {
    			// HTTP/0.9 style request
    			eol = true;
    			space = true;
    			end = pos;
    		} else if ((buf[pos] == Constants.QUESTION) && (questionPos == -1)) {
    			questionPos = pos;
    		} else if (HttpParser.isNotRequestTarget(buf[pos])) {
    			throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget"));
    		}
    
    		pos++;
    
    	}
    
    	request.unparsedURI().setBytes(buf, start, end - start);
    	if (questionPos >= 0) {
    		request.queryString().setBytes(buf, questionPos + 1,
    									   end - questionPos - 1);
    		request.requestURI().setBytes(buf, start, questionPos - start);
    	} else {
    		request.requestURI().setBytes(buf, start, end - start);
    	}
    
    	// Spec says single SP but also says be tolerant of multiple SP and/or HT
    	while (space) {
    		// Read new bytes if needed
    		if (pos >= lastValid) {
    			if (!fill())
    				throw new EOFException(sm.getString("iib.eof.error"));
    		}
    		if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
    			pos++;
    		} else {
    			space = false;
    		}
    	}
    
    	// Mark the current buffer position
    	start = pos;
    	end = 0;
    
    	//
    	// Reading the protocol
    	// Protocol is always "HTTP/" DIGIT "." DIGIT
    	//
    	while (!eol) {
    
    		// Read new bytes if needed
    		if (pos >= lastValid) {
    			if (!fill())
    				throw new EOFException(sm.getString("iib.eof.error"));
    		}
    
    		if (buf[pos] == Constants.CR) {
    			end = pos;
    		} else if (buf[pos] == Constants.LF) {
    			if (end == 0)
    				end = pos;
    			eol = true;
    		} else if (!HttpParser.isHttpProtocol(buf[pos])) {
    			// 关键点在这一句,如果校验不通过,则会报参数异常
    			throw new IllegalArgumentException(sm.getString("iib.invalidHttpProtocol"));
    		}
    
    		pos++;
    
    	}
    
    	if ((end - start) > 0) {
    		request.protocol().setBytes(buf, start, end - start);
    	} else {
    		request.protocol().setString("");
    	}
    
    	return true;
    
    }
    

    我们进一步跟进HttpParser中的方法:

    public static boolean isNotRequestTarget(int c) {
    	// Fast for valid request target characters, slower for some incorrect
    	// ones
    	try {
    		// 关键在于这个数组
    		return IS_NOT_REQUEST_TARGET[c];
    	} catch (ArrayIndexOutOfBoundsException ex) {
    		return true;
    	}
    }
    
    
    // Combination of multiple rules from RFC7230 and RFC 3986. Must be
    // ASCII, no controls plus a few additional characters excluded
    if (IS_CONTROL[i] || i > 127 ||
    		i == ' ' || i == '"' || i == '#' || i == '<' || i == '>' || i == '\' ||
    		i == '^' || i == '`'  || i == '{' || i == '|' || i == '}') {
    	// 可以看到只有在REQUEST_TARGET_ALLOW数组中的值才不会设置成true,所以我们需要追踪REQUEST_TARGET_ALLOW数组的赋值
    	if (!REQUEST_TARGET_ALLOW[i]) {
    		IS_NOT_REQUEST_TARGET[i] = true;
    	}
    }
    
    String prop = System.getProperty("tomcat.util.http.parser.HttpParser.requestTargetAllow");
    if (prop != null) {
    	for (int i = 0; i < prop.length(); i++) {
    		char c = prop.charAt(i);
    		// 可以看到在配置文件中配置了tomcat.util.http.parser.HttpParser.requestTargetAllow并且包含{、}、|的时候,REQUEST_TARGET_ALLOW数组中的值才会为true
    		if (c == '{' || c == '}' || c == '|') {
    			REQUEST_TARGET_ALLOW[c] = true;
    		} else {
    			log.warn(sm.getString("httpparser.invalidRequestTargetCharacter",
    					Character.valueOf(c)));
    		}
    	}
    }
    

        解决办法: 其实通过源码分析不难得到解决办法

    在Tomcat的catalina.properties文件中添加以下语句:

    tomcat.util.http.parser.HttpParser.requestTargetAllow={}|

    当然需要注意的是,这个后门在Tomcat8.5以后就无法使用的,Tomcat9之后的解决办法暂时未找到,可能只有对URL进行编码了。

    问题二:Cookie设置报错

         这个问题就是在升级到Tomcat8.5以上的时候会出现的,具体原因是Tomcat8.5采用的Cookie处理类是:

    Rfc6265CookieProcessor,而在之前使用的处理类是LegacyCookieProcessor。该处理类对domai进行了校验:

    private void validateDomain(String domain) {
    	int i = 0;
    	int prev = -1;
    	int cur = -1;
    	char[] chars = domain.toCharArray();
    	while (i < chars.length) {
    		prev = cur;
    		cur = chars[i];
    		if (!domainValid.get(cur)) {
    			throw new IllegalArgumentException(sm.getString(
    					"rfc6265CookieProcessor.invalidDomain", domain));
    		}
    		// labels must start with a letter or number
    		if ((prev == '.' || prev == -1) && (cur == '.' || cur == '-')) {
    			throw new IllegalArgumentException(sm.getString(
    					"rfc6265CookieProcessor.invalidDomain", domain));
    		}
    		// labels must end with a letter or number
    		if (prev == '-' && cur == '.') {
    			throw new IllegalArgumentException(sm.getString(
    					"rfc6265CookieProcessor.invalidDomain", domain));
    		}
    		i++;
    	}
    	// domain must end with a label
    	if (cur == '.' || cur == '-') {
    		throw new IllegalArgumentException(sm.getString(
    				"rfc6265CookieProcessor.invalidDomain", domain));
    	}
    }
    

    新的Cookie规范对domain有以下要求

    1、必须是1-9、a-z、A-Z、. 、- (注意是-不是_)这几个字符组成
    2、必须是数字或字母开头 (所以以前的cookie的设置为.XX.com 的机制要改为 XX.com 即可)
    3、必须是数字或字母结尾

    原来的代码设置domain时如下:

    cookie.setDomain(".aaa.com");
    

    这就导致设置domain的时候不符合新的规范,直接报错如下:

    java.lang.IllegalArgumentException: An invalid domain [.aaa.com] was specified for this cookie
            at org.apache.tomcat.util.http.Rfc6265CookieProcessor.validateDomain(Rfc6265CookieProcessor.java:181)
            at org.apache.tomcat.util.http.Rfc6265CookieProcessor.generateHeader(Rfc6265CookieProcessor.java:123)
            at org.apache.catalina.connector.Response.generateCookieString(Response.java:989)
            at org.apache.catalina.connector.Response.addCookie(Response.java:937)
            at org.apache.catalina.connector.ResponseFacade.addCookie(ResponseFacade.java:386)
    

        解决办法(以下3中任意一种皆可)

    1. 修改原来代码为:

      cookie.setDomain("aaa.com");
      
    2. 如果是Spring-boot环境,直接替换默认的Cookie处理类:

      @Configuration
      @ConditionalOnExpression("${tomcat.useLegacyCookieProcessor:false}")
      public class LegacyCookieProcessorConfiguration {
          @Bean
          EmbeddedServletContainerCustomizer embeddedServletContainerCustomizerLegacyCookieProcessor() {
              return new EmbeddedServletContainerCustomizer() {
                  @Override
                  public void customize(ConfigurableEmbeddedServletContainer factory) {
                      if (factory instanceof TomcatEmbeddedServletContainerFactory) {
                          TomcatEmbeddedServletContainerFactory tomcatFactory =
                                  (TomcatEmbeddedServletContainerFactory) factory;
                          tomcatFactory.addContextCustomizers(new TomcatContextCustomizer() {
                              @Override
                              public void customize(Context context) {
                                  context.setCookieProcessor(new LegacyCookieProcessor());
                              }
                          });
                      }
                  }
              };
          }
      }
      
    3. 在Tomcat的context.xml中增加如下配置,指定Cookie的处理类:

      <CookieProcessor className="org.apache.tomcat.util.http.LegacyCookieProcessor" /> 
      

    参考链接

    https://blog.csdn.net/fy_sun123/article/details/73115381

    http://ju.outofmemory.cn/entry/367186

    https://www.cnblogs.com/lr393993507/p/7755867.html
    http://tomcat.apache.org/tomcat-8.5-doc/config/cookie-processor.html

  • 相关阅读:
    测试是否有必要看开发代码?如何能看懂?
    【LeetCode】111. 二叉树的最小深度(BFS 解题套路框架,要会默写)
    【LeetCode】112. 路径总和
    【测试开发】知识点配置 Nginx 解决多端口访问
    【测试开发】知识点使用EasyExcel,实现excel导出和导入
    p5 随机圆连接背景和代码树
    angular技巧
    javascript原生技巧篇
    MybatisPlus
    安装 jupyter notebook
  • 原文地址:https://www.cnblogs.com/Kidezyq/p/10450332.html
Copyright © 2011-2022 走看看