在Java中可以使用java.net.URI处理的主机名是RFC 2396(不是RFC 3986)
今天我稍微研究了一下有关URL的问题,发现java.net.URI不是按照RFC 3986标准制定的,而是按照RFC 2396制定的。而且,导致我遇到问题的原因就是RFC 2396和RFC 3986之间的差异。
-
- http://www.ietf.org/rfc/rfc2396.txt
-
- http://www.ietf.org/rfc/rfc3986.txt
- https://docs.oracle.com/javase/jp/8/docs/api/java/net/URI.html
环境
-
- Java SE 8
-
- Spring Boot 1.3.5.RELEASE (Tomcat 8.0)
- Nginx 1.9.?
不同之处是…
如果您想了解具体差异,请参考RFC。我遇到的问题是关于主机名的部分。
在RFC 2396中,对于主机名的定义如下:
host = hostname | IPv4address
# 以降、IPアドレス系の定義は省略します
hostname = *( domainlabel "." ) toplabel [ "." ]
domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
toplabel = alpha | alpha *( alphanum | "-" ) alphanum
在RFC 3986中,要求仅允许”半角英数字”、”-“和”.”。
host = IP-literal / IPv4address / reg-name
# 以降、IPアドレス系の定義は省略します
reg-name = *( unreserved / pct-encoded / sub-delims )
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
pct-encoded = "%" HEXDIG HEXDIG
sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "="
已经变得相当熟练。对于能够处理的文字数量也有所增加。
迷上了網絡(綁定)
目前参与的项目中,Web服务器采用了”Nginx”,应用程序使用Spring Boot(内嵌Tomcat)的构建。在Nginx向Spring Boot代理请求时,使用了upstream指令。(我实际上以前从未使用过Nginx,今天是第一次知道它……)
在配置上,大致是这样的。(省略了无关部分+实际上不是localhost)
http {
upstream spring_boot {
server localhost:8080;
}
server {
listen 18080;
server_name localhost;
location /spring-boot/ {
proxy_pass http://spring_boot/;
}
}
}
在这种情况下,向Nginx发送请求(http://localhost:18080/spring-boot/),则转发到Spring Boot端的请求(http://localhost:8080/)的Host头部将变为“spring_boot”(上游名称)。Servlet API的HttpServletRequest#getRequestURL方法会查看Host头部并返回URL,因此在此情况下将变为“http://spring_boot”(由于RFC 2396中不允许使用字符“_”作为主机名的原因)。
那么,实际上出现问题的地方是…
Spring Framework提供了一个处理URL的工具,名为org.springframework.web.util.UriComponentsBuilder。在该类的fromHttpRequest方法中,我们使用HttpServletRequest的getRequestURL方法获得的URL字符串来创建一个java.net.URI的实例。结果是什么呢?URI的getHost方法返回null,因此无法获取主机名。
为什么是这样的原因呢……
直接的原因是在Java标准的URI类中传递了包含无法处理的字符的URL,但根本原因是Nginx的配置。然而……不能保证Spring Framework没有任何问题,因为在同一个类中的 “传递URI字符串的方法(UriComponentsBuilder#fromUriString(String)或fromHttpUrl())”似乎支持RFC 3986。另外,Servlet API本身并不依赖于java.net.URI,所以Spring Framework的实现可能导致了这个错误。
对策是…
首先,“主机名必须符合RFC 2396规范!!!” 这个最重要。同时,保持上游名称也符合RFC 2396规范是安全的做法。
然后…
我觉得将Host头部设置为上游名称是基本不可行的,所以我认为最好的做法是在访问Nginx时指定要保留的主机名。如果要在Host头部保留访问Nginx时指定的主机名,则可以使用以下的proxy_set_header指令。
location /spring-boot/ {
proxy_pass http://spring-boot/;
proxy_set_header host $host;
}
另外,UriComponentsBuilder#fromHttpRequest(HttpRequest)方法是根据存在的“Forwarded”、“X-Forwarded-Host”、“X-Forwarded-Port”、“X-Forwarded-Proto”头部获取方案、主机名和端口号的机制,因此也可以利用这些头部来进行操作。
已知的发生条件是…
这个事件不一定会发生。已知的发生条件如下:
- リクエストにOriginヘッダーがあるとCORS(Cross-Origin Resource Sharing)関連の処理が動き、その処理の中でUriComponentsBuilder#fromHttpRequestメソッドを使用しており、後続処理でホスト名を参照しているところでNullpointerExceptionが発生します。
...
2016-05-25 04:46:48.890 ERROR 84097 --- [io-8080-exec-10] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.NullPointerException] with root cause
java.lang.NullPointerException: null
at org.springframework.web.util.WebUtils.isSameOrigin(WebUtils.java:816) ~[spring-web-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.web.cors.DefaultCorsProcessor.processRequest(DefaultCorsProcessor.java:76) ~[spring-web-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.web.servlet.handler.AbstractHandlerMapping$CorsInterceptor.preHandle(AbstractHandlerMapping.java:503) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.web.servlet.HandlerExecutionChain.applyPreHandle(HandlerExecutionChain.java:134) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:956) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:895) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:967) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:858) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:622) ~[tomcat-embed-core-8.0.33.jar:8.0.33]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:843) ~[spring-webmvc-4.2.6.RELEASE.jar:4.2.6.RELEASE]
...
总结
没有特别需要总结的事情,但是如果要组合Nginx + Spring Boot(Spring MVC),也许将这次发布的内容记在脑海中会有帮助。
补充
2016/5/26
关于CORS(跨域资源共享)相关的处理出现错误的问题,是由于Spring Framework的一个错误。