在最近的項(xiàng)目中,遇到這樣一個(gè)場(chǎng)景:合作方開(kāi)發(fā)H5頁(yè)面并部署在合作方的服務(wù)器上,但頁(yè)面中嵌入了我方的SDK,SDK會(huì)直接調(diào)用我方的接口,如下圖:
但是控制臺(tái)中卻會(huì)收到如下報(bào)錯(cuò):
Access to XMLHttpRequest at 'http://example1.com/test' from origin 'http://example2.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
這就是跨域的報(bào)錯(cuò)。
跨域是什么
跨域,是指瀏覽器不能執(zhí)行其它網(wǎng)站的腳本。它是由瀏覽器的同源策略造成的,是瀏覽器對(duì)JAVAscript實(shí)施的安全限制。
簡(jiǎn)單來(lái)講,就是從地址A加載的頁(yè)面,不能訪問(wèn)地址B的服務(wù)(如上圖)。此時(shí)地址A與地址B不同源。
所謂同源,就是域名、協(xié)議、端口均相同。舉個(gè)例子:
http://www.123.com/index.html 調(diào)用 http://www.123.com/abc.do (非跨域)
http://www.123.com/index.html 調(diào)用 http://www.456.com/abc.do (主域名不同:123/456,跨域)
http://abc.123.com/index.html 調(diào)用 http://def.123.com/server.do (子域名不同:abc/def,跨域)
http://www.123.com:8080/index.html 調(diào)用 http://www.123.com:8081/server.do(端口不同:8080/8081,跨域)
http://www.123.com/index.html 調(diào)用 https://www.123.com/server.do (協(xié)議不同:http/https,跨域)
如上所述,由于合作方的域名與我方的域名不同,從合作方加載的頁(yè)面,調(diào)用我方接口的時(shí)候,就會(huì)出現(xiàn)跨域的報(bào)錯(cuò)。
是否有辦法可以解決這個(gè)問(wèn)題呢,需要從CORS說(shuō)起。
CORS
隨著互聯(lián)網(wǎng)的發(fā)展,同源策略嚴(yán)重影響了項(xiàng)目之間的連接,尤其是大項(xiàng)目,需要多個(gè)域名配合完成,因此W3C推出了CORS,即Cross-origin resource sharing(跨來(lái)源資源共享)。CORS的基本思想就是使用額外的HTTP頭部讓瀏覽器與服務(wù)器進(jìn)行溝通,從而決定是否接受跨域請(qǐng)求。
CORS需要瀏覽器和服務(wù)器同時(shí)支持,目前,所有瀏覽器都支持該功能。對(duì)于開(kāi)發(fā)者來(lái)說(shuō),CORS通信與同源的AJAX通信沒(méi)有區(qū)別,代碼完全一樣。瀏覽器在跨域訪問(wèn)時(shí),會(huì)自動(dòng)添加HTTP頭信息,或者發(fā)起預(yù)檢請(qǐng)求,用戶對(duì)此毫無(wú)感知。因此是否支持跨域請(qǐng)求,關(guān)鍵在于服務(wù)器是否做了CORS配置,允許跨域訪問(wèn)。
瀏覽器將跨域請(qǐng)求分為兩類(lèi):簡(jiǎn)單請(qǐng)求和非簡(jiǎn)單請(qǐng)求。
同時(shí)滿足以下兩大條件的,就屬于簡(jiǎn)單請(qǐng)求:
- 請(qǐng)求方法是以下3種之一:GETPOSTHEAD
- HTTP頭信息不超出以下字段:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type:僅限于三個(gè)值A(chǔ)pplication/x-www-form-urlencoded、multipart/form-data、text/plain
凡是不滿足以上條件的,就屬于非簡(jiǎn)單請(qǐng)求。如我們常用的json格式請(qǐng)求,由于其Content-Type的值為application/json,因此屬于非簡(jiǎn)單請(qǐng)求。
對(duì)于這兩種請(qǐng)求,瀏覽器的處理方式是不一樣的。
簡(jiǎn)單請(qǐng)求
對(duì)于簡(jiǎn)單請(qǐng)求,瀏覽器采用先請(qǐng)求后判斷的方式,即瀏覽器直接發(fā)出CORS請(qǐng)求,即在請(qǐng)求頭中增加Origin字段,如圖:
Origin字段用來(lái)向服務(wù)器說(shuō)明,本次請(qǐng)求來(lái)自于哪個(gè)源(協(xié)議+域名+端口),服務(wù)器決定是否允許這個(gè)源的訪問(wèn)。
服務(wù)器判斷該源如果不在自己允許的范圍內(nèi),就返回一個(gè)正常的HTTP響應(yīng)。瀏覽器判斷響應(yīng)頭中是否包含Access-Control-Allow-Origin字段,如果沒(méi)有,瀏覽器就知道服務(wù)器是不允許跨域訪問(wèn)的,就會(huì)拋出錯(cuò)誤。
如果Origin在服務(wù)器允許的范圍內(nèi),服務(wù)器的HTTP響應(yīng)中,就會(huì)包含如下字段:
Access-Control-Allow-Origin 它的值要么是請(qǐng)求時(shí)Origin字段的值,要么是一個(gè)*(表示接受任意域名的請(qǐng)求)。 Access-Control-Allow-Credentials 它的值是一個(gè)布爾值,表示是否允許發(fā)送Cookie。默認(rèn)情況下,Cookie不包括在CORS請(qǐng)求之中。設(shè)為true,即表示服務(wù)器明確許可,Cookie可以包含在請(qǐng)求中,一起發(fā)給服務(wù)器。 Access-Control-Allow-Headers 允許瀏覽器在CORS中發(fā)送的頭信息。 Access-Control-Allow-Methods
允許瀏覽器在CORS中使用的方法。
瀏覽器收到服務(wù)器返回的HTTP響應(yīng)后,即可知道什么樣的CORS請(qǐng)求是被允許的。
非簡(jiǎn)單請(qǐng)求
對(duì)于非簡(jiǎn)單請(qǐng)求,瀏覽器采用預(yù)檢請(qǐng)求,詢問(wèn)服務(wù)器是否支持跨域請(qǐng)求。在正式的請(qǐng)求之前,瀏覽器會(huì)預(yù)先發(fā)送一個(gè)額外的OPTIONS請(qǐng)求,詢問(wèn)服務(wù)器當(dāng)前網(wǎng)頁(yè)所在的域名是否在服務(wù)器的許可名單之中,以及可以使用哪些HTTP方法和頭字段。只有得到肯定答復(fù),瀏覽器才會(huì)發(fā)出正式的XMLHttpRequest請(qǐng)求,否則就報(bào)錯(cuò)。如圖:
HTTP正式請(qǐng)求的方法是POST,并且發(fā)送一個(gè)頭信息content-type(本例中使用content-type=application/json,因此是非簡(jiǎn)單請(qǐng)求)。
服務(wù)器收到預(yù)檢請(qǐng)求之后,檢查Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段,并做出響應(yīng),如下圖:
Access-Control-Max-Age
用來(lái)指定本次預(yù)檢請(qǐng)求的有效期,單位為秒。上面結(jié)果中,有效期是3600秒,即允許緩存該條回應(yīng)3600秒,在此期間,可直接發(fā)送正式請(qǐng)求,不用再發(fā)預(yù)檢請(qǐng)求。
在上圖例中,瀏覽器請(qǐng)求Origin是http://192.168.47.130,服務(wù)器響應(yīng)Access-Control-Allow-Origin是http://127.0.0.1,因此瀏覽器會(huì)報(bào)錯(cuò)。只有在服務(wù)器響應(yīng)與瀏覽器的請(qǐng)求內(nèi)容相匹配,瀏覽器才不報(bào)錯(cuò)。
跨域的解決辦法
遇到跨域的報(bào)錯(cuò),可以分別從客戶端和服務(wù)端去解決。
客戶端
通過(guò)上面的分析可以知道,跨域的判斷是在瀏覽器進(jìn)行的,服務(wù)器只是根據(jù)客戶端的請(qǐng)求做出正常的響應(yīng),服務(wù)端不對(duì)跨域做任何判斷。因此如果禁用了瀏覽器的跨域檢查,使瀏覽器不再對(duì)比Origin是否被服務(wù)器允許,即可發(fā)出正常的請(qǐng)求。
該方式需要所有客戶都修改瀏覽器的設(shè)置,顯然是不現(xiàn)實(shí)的,因此只在開(kāi)發(fā)調(diào)試的過(guò)程中使用,如給chrome瀏覽器設(shè)置--disable-web-security參數(shù)。
服務(wù)端
服務(wù)端又有兩種解決方式:代理轉(zhuǎn)發(fā)和配置CORS。
代理轉(zhuǎn)發(fā)
代理轉(zhuǎn)發(fā)的架構(gòu)如下:
增加代理服務(wù)器,和H5資源服務(wù)器放在同一個(gè)域名下,接口請(qǐng)求全走代理服務(wù)器,這樣就變成了同源訪問(wèn),不存在跨域訪問(wèn),因此就不會(huì)存在跨域的問(wèn)題。
該方式中,所有發(fā)往目標(biāo)服務(wù)器的數(shù)據(jù),都會(huì)經(jīng)過(guò)代理服務(wù)器,適用于同一個(gè)公司內(nèi)部不同域名之間相互訪問(wèn)的情況。但對(duì)于我們這個(gè)項(xiàng)目,由SDK發(fā)往我方服務(wù)器的數(shù)據(jù)是敏感數(shù)據(jù),需客戶端直接發(fā)往我方服務(wù)器上,不能由合作方做代理轉(zhuǎn)發(fā),因此不能使用此種方式。
使用此方式還需注意一點(diǎn),應(yīng)關(guān)注代理服務(wù)器的性能,代理服務(wù)器的性能應(yīng)與后端的目標(biāo)服務(wù)器的性能相匹配,否則代理服務(wù)器會(huì)成為整個(gè)系統(tǒng)的性能瓶頸。
配置CORS
在目標(biāo)服務(wù)器上配置CORS響應(yīng)頭,這樣瀏覽器經(jīng)過(guò)對(duì)比判斷之后,就可以發(fā)起正常的訪問(wèn)。
目標(biāo)服務(wù)一般是由軟負(fù)載和應(yīng)用服務(wù)組成(如常見(jiàn)的Apache+jboss,Nginx+Tomcat等組合),在軟負(fù)載和應(yīng)用上都可添加CORS響應(yīng)頭。
如在apache的httpd.conf中添加如下配置:
Header set Access-Control-Allow-Origin *
//或者Header set Access-Control-Allow-Origin http://xxx.com
Header set Access-Control-Allow-Methods POST,GET
Header set Access-Control-Allow-Headers *
或者nginx的配置中增加如下配置:
location / {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
if ($request_method = 'OPTIONS') {
return 204;
}
}
此方式的優(yōu)點(diǎn)是不用修改應(yīng)用代碼,缺點(diǎn)是不能做細(xì)粒度的編程,從而做到細(xì)粒度的控制,如根據(jù)請(qǐng)求參數(shù)的不同而返回不同的結(jié)果。 另一種方式,就是修改應(yīng)用代碼。通常是在服務(wù)器代碼中增加filter,在filter中在HTTP響應(yīng)頭添加相應(yīng)的字段,如下:
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
logger.debug("CorsFilter ----> doFilter");
HttpServletResponse res = (HttpServletResponse) servletResponse;
HttpServletRequest req = (HttpServletRequest) servletRequest;
//只允許 http 或 https 開(kāi)頭域名的請(qǐng)求
String origin = req.getHeader("Origin");
if (StringUtils.isNotEmpty(origin) && (origin.toLowerCase(Locale.ENGLISH).startsWith("http")
|| origin.toLowerCase(Locale.ENGLISH).startsWith("https"))) {
res.addHeader("Access-Control-Allow-Origin", origin);
}
res.addHeader("Access-Control-Allow-Methods", ALLOWED_METHODS);
res.addHeader("Access-Control-Allow-Headers",ALLOWED_HEADERS);
res.addHeader("Access-Control-Allow-Credentials", "true");
if(((HttpServletRequest) servletRequest).getMethod().equals(HttpMethod.OPTIONS.name())){
res.addHeader("Access-Control-Max-Age", "3600");
((HttpServletResponse) servletResponse).setStatus(200);
return ;
}
filterChain.doFilter(servletRequest, servletResponse);
}
由于是通過(guò)代碼控制,因此可以實(shí)現(xiàn)細(xì)粒度的控制,在解決跨域問(wèn)題的同時(shí),可以滿足復(fù)雜的業(yè)務(wù)需求。