目录
环境信息
Spring Boot:2.0.8.RELEASE
Spring Boot内置的tomcat:tomcat-embed-core 8.5.37
问题描述
在使用浏览器访问应用,给服务端发送请求的时候,前端没有收到正确的响应,浏览器控制台和网络都报错了:
解决方案
解决思路
乍一看是CORS跨域问题,可是服务端已经进行了CORS跨域全部放行设置,莫非是不生效了或者被回退了。
按这个思路排查发现不是因为cors导致的,cors的跨域设置还是生效的。服务器也没问题,启动日志正常。
后面仔细看了下报错信息,发现请求的url路径不大对,根路径不对!
请求的正确路径是:
http://localhost:9003/tfb-remt-biz-service-app/trade/tradeService
备注:现实里发生的错误,请求路径不是图片里那样,而是
http://localhost:9003/remt-biz-service-app/trade/tradeService
为了使案例的错误明显些,将请求改成了
http://localhost:9003/tfb-remt-biz-service-app2/trade/tradeService
解决方法
将请求的路径改成正确的之后,成功收到服务端的返回信息。
不同的路径错误现象
这次的错误,浏览器提示的信息很有误导性,印象中的路径不对应该是直接返回404,而不会出现CORS跨域错误。
为了深入了解预检请求preflight返回404,导致CORS跨域错误的原因,分析了下源码,并和普通的路径不对返回404进行了对比
上下文路径不对
如果是应用的上下文路径不对,比如tfb-remt-biz-service-app写成了tfb-remt-biz-service-app2
http://localhost:9003/tfb-remt-biz-service-app2/trade/tradeService
复杂请求的时候,会先发送预检请求,再发送真正的请求。
以下是2个请求的结果:
chrome浏览器
网络
只筛选Fetch/XHR:显示的结果是CORS错误,很容易被误导成CORS的设置有问题,比较难发现是404请求路径错误
不筛选,显示全部网络请求:
控制台
火狐
网络
控制台
第一个请求:真实请求
这个请求的真正处理是在预检请求preflight返回之后的。
返回的状态是CORS错误,而不是404
第二个请求:预检请求preflight
请求方法是OPTIONS,需要注意的是返回信息:响应体是空,响应头里没有包含了Access-Control-Allow-Origin,返回的状态是404
非上下文的路径不对
如果访问的url里,上下文路径是对的,后面的路径不对,比如tradeService写成了tradeService2,如下图所示;
http://localhost:9003/tfb-remt-biz-service-app/trade/tradeService2
复杂请求的时候,会先发送预检请求,再发送真正的请求
以下是2个请求的结果:
chrome浏览器
网络
注意如果有筛选了请求类型,要先去掉筛选,不然如果没有选中其他,那么预检请求(prefight)不会显示
控制台
第一个请求:预检请求preflight
请求方法是OPTIONS,需要注意的是返回信息:响应体是空,响应头里包含了Access-Control-Allow-Origin: http://localhost:9999,返回的状态是200
服务端的请求处理链路:在CorsFilter里进行了跨域处理,因为服务端设置了允许跨域,因此响应头里包含了允许跨域Access-Control-Allow-Origin。
服务端跨域设置:
服务端的请求处理链路:
第二个请求:真实请求
因为找不到该路径对应的服务端接口,返回的状态是404。
源码分析
调用链
CoyoteAdapter.java
在postParseRequest方法里执行了
connector.getService().getMapper().map(serverName, decodedURI,
version, request.getMappingData());
这里的取得的mapper是 org.apache.catalina.mapper.Mapper
Mapper.java
contexts的值:
contexts= {Mapper$MappedContext[1]@24469}
0 = {Mapper$MappedContext@24470}
versions = {Mapper$ContextVersion[1]@24472}
name = "/tfb-remt-biz-service-app"
object = null
根据uri来和contexts上下文路径/根路径来进行匹配
根路径:/tfb-remt-biz-service-app
1.如果请求路径的上下文路径不对,比如:
http://localhost:9003/tfb-remt-biz-service-app2/trade/tradeService
那么匹配不到,found是false。
2.如果请求路径的上下文路径是对的,其余路径不对,比如:
http://localhost:9003/tfb-remt-biz-service-app/trade/tradeService2
那么是可以匹配到的,found是true。
匹配逻辑
while (pos >= 0) {
context = contexts[pos];
if (uri.startsWith(context.name)) {
length = context.name.length();
if (uri.getLength() == length) {
found = true;
break;
} else if (uri.startsWithIgnoreCase("/", length)) {
found = true;
break;
}
}
if (lastSlash == -1) {
lastSlash = nthSlash(uri, contextList.nesting + 1);
} else {
lastSlash = lastSlash(uri);
}
uri.setEnd(lastSlash);
pos = find(contexts, uri);
}
匹配不到,在这里返回了,没有往下执行
下列的语句没有执行到,因此请求的上下文对象request.getContext()还是空的:
mappingData.context = contextVersion.object;
mappingData.contextSlashCount = contextVersion.slashCount;
CoyoteAdapter.java
执行完
connector.getService().getMapper().map(serverName, decodedURI,
version, request.getMappingData());
之后,request.getContext() == null,直接返回了404,不再进行后续的处理,包括CORS过滤器,因此响应头里是没有Access-Control-Allow-Origin的
总结
tomcat在处理请求(包括预检请求)的时候,会首先对请求的uri和服务的上下文路径/根路径进行匹配,
如果匹配到了,才继续进行后续的CORS过滤器等处理,并可能携带允许跨域的响应头,真实请求不会出现CORS;
如果匹配不到,那么直接返回404,不会携带允许跨域的响应头,造成了真实请求出现CORS错误,而不是404错误。