背景
在最近的項(xiàng)目中讲婚,遇到這樣一個場景:合作方開發(fā)H5頁面并部署在合作方的服務(wù)器上浪秘,但頁面中嵌入了我方的SDK评架,SDK會直接調(diào)用我方的接口,如下圖:
但是控制臺中卻會收到如下報錯:
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.
這就是跨域的報錯锨亏。
跨域是什么
跨域痴怨,是指瀏覽器不能執(zhí)行其它網(wǎng)站的腳本。它是由瀏覽器的同源策略造成的器予,是瀏覽器對javascript實(shí)施的安全限制浪藻。
簡單來講,就是從地址A加載的頁面劣摇,不能訪問地址B的服務(wù)(如上圖)珠移。此時地址A與地址B不同源。
所謂同源,就是域名钧惧、協(xié)議暇韧、端口均相同。舉個例子:
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乾颁,跨域)
如上所述涂乌,由于合作方的域名與我方的域名不同,從合作方加載的頁面英岭,調(diào)用我方接口的時候湾盒,就會出現(xiàn)跨域的報錯。
是否有辦法可以解決這個問題呢诅妹,需要從CORS說起罚勾。
CORS
隨著互聯(lián)網(wǎng)的發(fā)展,同源策略嚴(yán)重影響了項(xiàng)目之間的連接吭狡,尤其是大項(xiàng)目尖殃,需要多個域名配合完成,因此W3C推出了CORS划煮,即Cross-origin resource sharing(跨來源資源共享)送丰。CORS的基本思想就是使用額外的HTTP頭部讓瀏覽器與服務(wù)器進(jìn)行溝通,從而決定是否接受跨域請求弛秋。
CORS需要瀏覽器和服務(wù)器同時支持器躏,目前,所有瀏覽器都支持該功能铐懊。對于開發(fā)者來說邀桑,CORS通信與同源的AJAX通信沒有區(qū)別,代碼完全一樣科乎。瀏覽器在跨域訪問時,會自動添加HTTP頭信息贼急,或者發(fā)起預(yù)檢請求茅茂,用戶對此毫無感知。因此是否支持跨域請求太抓,關(guān)鍵在于服務(wù)器是否做了CORS配置空闲,允許跨域訪問。
瀏覽器將跨域請求分為兩類:簡單請求和非簡單請求走敌。
同時滿足以下兩大條件的碴倾,就屬于簡單請求:
- 請求方法是以下3種之一:
- GET
- POST
- HEAD
- HTTP頭信息不超出以下字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:僅限于三個值application/x-www-form-urlencoded、multipart/form-data、text/plain
凡是不滿足以上條件的跌榔,就屬于非簡單請求异雁。如我們常用的json格式請求,由于其Content-Type的值為application/json僧须,因此屬于非簡單請求纲刀。
對于這兩種請求,瀏覽器的處理方式是不一樣的担平。
簡單請求
對于簡單請求示绊,瀏覽器采用先請求后判斷的方式,即瀏覽器直接發(fā)出CORS請求暂论,即在請求頭中增加Origin字段面褐,如圖:
Origin字段用來向服務(wù)器說明,本次請求來自于哪個源(協(xié)議+域名+端口)取胎,服務(wù)器決定是否允許這個源的訪問盆耽。
服務(wù)器判斷該源如果不在自己允許的范圍內(nèi),就返回一個正常的HTTP響應(yīng)扼菠。瀏覽器判斷響應(yīng)頭中是否包含Access-Control-Allow-Origin字段摄杂,如果沒有,瀏覽器就知道服務(wù)器是不允許跨域訪問的循榆,就會拋出錯誤析恢。
如果Origin在服務(wù)器允許的范圍內(nèi),服務(wù)器的HTTP響應(yīng)中秧饮,就會包含如下字段:
Access-Control-Allow-Origin
它的值要么是請求時Origin字段的值映挂,要么是一個*(表示接受任意域名的請求)。
Access-Control-Allow-Credentials
它的值是一個布爾值盗尸,表示是否允許發(fā)送Cookie柑船。默認(rèn)情況下,Cookie不包括在CORS請求之中泼各。設(shè)為true鞍时,即表示服務(wù)器明確許可,Cookie可以包含在請求中扣蜻,一起發(fā)給服務(wù)器逆巍。
Access-Control-Allow-Headers
允許瀏覽器在CORS中發(fā)送的頭信息。
Access-Control-Allow-Methods
允許瀏覽器在CORS中使用的方法莽使。
瀏覽器收到服務(wù)器返回的HTTP響應(yīng)后锐极,即可知道什么樣的CORS請求是被允許的。
非簡單請求
對于非簡單請求芳肌,瀏覽器采用預(yù)檢請求灵再,詢問服務(wù)器是否支持跨域請求肋层。在正式的請求之前,瀏覽器會預(yù)先發(fā)送一個額外的OPTIONS請求翎迁,詢問服務(wù)器當(dāng)前網(wǎng)頁所在的域名是否在服務(wù)器的許可名單之中栋猖,以及可以使用哪些HTTP方法和頭字段。只有得到肯定答復(fù)鸳兽,瀏覽器才會發(fā)出正式的XMLHttpRequest請求掂铐,否則就報錯。如圖:
HTTP正式請求的方法是POST揍异,并且發(fā)送一個頭信息content-type(本例中使用content-type=application/json全陨,因此是非簡單請求)。
服務(wù)器收到預(yù)檢請求之后衷掷,檢查Origin辱姨、Access-Control-Request-Method和Access-Control-Request-Headers字段,并做出響應(yīng)戚嗅,如下圖:
Access-Control-Max-Age
用來指定本次預(yù)檢請求的有效期雨涛,單位為秒。上面結(jié)果中懦胞,有效期是3600秒替久,即允許緩存該條回應(yīng)3600秒,在此期間躏尉,可直接發(fā)送正式請求蚯根,不用再發(fā)預(yù)檢請求。
在上圖例中胀糜,瀏覽器請求Origin是http://192.168.47.130颅拦,服務(wù)器響應(yīng)Access-Control-Allow-Origin是http://127.0.0.1,因此瀏覽器會報錯教藻。只有在服務(wù)器響應(yīng)與瀏覽器的請求內(nèi)容相匹配距帅,瀏覽器才不報錯。
跨域的解決辦法
遇到跨域的報錯括堤,可以分別從客戶端和服務(wù)端去解決碌秸。
客戶端
通過上面的分析可以知道,跨域的判斷是在瀏覽器進(jìn)行的痊臭,服務(wù)器只是根據(jù)客戶端的請求做出正常的響應(yīng)哮肚,服務(wù)端不對跨域做任何判斷。因此如果禁用了瀏覽器的跨域檢查广匙,使瀏覽器不再對比Origin是否被服務(wù)器允許,即可發(fā)出正常的請求恼策。
該方式需要所有客戶都修改瀏覽器的設(shè)置鸦致,顯然是不現(xiàn)實(shí)的潮剪,因此只在開發(fā)調(diào)試的過程中使用,如給chrome瀏覽器設(shè)置--disable-web-security參數(shù)分唾。
服務(wù)端
服務(wù)端又有兩種解決方式:代理轉(zhuǎn)發(fā)和配置CORS抗碰。
代理轉(zhuǎn)發(fā)
代理轉(zhuǎn)發(fā)的架構(gòu)如下:
增加代理服務(wù)器,和H5資源服務(wù)器放在同一個域名下绽乔,接口請求全走代理服務(wù)器弧蝇,這樣就變成了同源訪問,不存在跨域訪問折砸,因此就不會存在跨域的問題看疗。
該方式中,所有發(fā)往目標(biāo)服務(wù)器的數(shù)據(jù)睦授,都會經(jīng)過代理服務(wù)器两芳,適用于同一個公司內(nèi)部不同域名之間相互訪問的情況。但對于我們這個項(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ù)器會成為整個系統(tǒng)的性能瓶頸。
配置CORS
在目標(biāo)服務(wù)器上配置CORS響應(yīng)頭猎荠,這樣瀏覽器經(jīng)過對比判斷之后坚弱,就可以發(fā)起正常的訪問。
目標(biāo)服務(wù)一般是由軟負(fù)載和應(yīng)用服務(wù)組成(如常見的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ù)請求參數(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 開頭域名的請求
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);
}
由于是通過代碼控制,因此可以實(shí)現(xiàn)細(xì)粒度的控制罪帖,在解決跨域問題的同時促煮,可以滿足復(fù)雜的業(yè)務(wù)需求邮屁。
總結(jié)
- 跨域是由瀏覽器的同源策略造成的,所謂同源菠齿,即域名佑吝、協(xié)議、端口均相同绳匀。
- CORS(跨來源資源共享)芋忿,通過添加HTTP頭信息,使瀏覽器判斷是否可以發(fā)起跨域訪問疾棵。
- 瀏覽器將跨域請求分為兩類:簡單請求和非簡單請求戈钢。簡單請求采取先請求后判斷的方式,非簡單請求采取預(yù)檢請求的方式判斷是否允許跨域訪問陋桂。
- 解決跨域通常采用服務(wù)端代理轉(zhuǎn)發(fā)和配置CORS兩種方式逆趣。
文章推薦:
基于事件驅(qū)動架構(gòu)的用戶成長體系
搞懂dubbo的SPI擴(kuò)展機(jī)制
關(guān)注【程序員順仔】,回復(fù)【資料】嗜历,即可獲得一套架構(gòu)進(jìn)階電子書籍宣渗。