聊聊跨域的原理與解決方法

背景

在最近的項(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)階電子書籍宣渗。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市梨州,隨后出現(xiàn)的幾起案子痕囱,更是在濱河造成了極大的恐慌,老刑警劉巖暴匠,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鞍恢,死亡現(xiàn)場離奇詭異,居然都是意外死亡每窖,警方通過查閱死者的電腦和手機(jī)帮掉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來窒典,“玉大人蟆炊,你說我怎么就攤上這事∑僦荆” “怎么了涩搓?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長劈猪。 經(jīng)常有香客問我昧甘,道長,這世上最難降的妖魔是什么战得? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任充边,我火速辦了婚禮,結(jié)果婚禮上常侦,老公的妹妹穿的比我還像新娘痛黎。我一直安慰自己予弧,他們只是感情好刮吧,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布湖饱。 她就那樣靜靜地躺著,像睡著了一般杀捻。 火紅的嫁衣襯著肌膚如雪井厌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天致讥,我揣著相機(jī)與錄音仅仆,去河邊找鬼。 笑死垢袱,一個胖子當(dāng)著我的面吹牛墓拜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播请契,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼咳榜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了爽锥?” 一聲冷哼從身側(cè)響起涌韩,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎氯夷,沒想到半個月后臣樱,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡腮考,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年雇毫,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片踩蔚。...
    茶點(diǎn)故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡棚放,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出寂纪,到底是詐尸還是另有隱情席吴,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布捞蛋,位于F島的核電站孝冒,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏拟杉。R本人自食惡果不足惜庄涡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望搬设。 院中可真熱鬧穴店,春花似錦撕捍、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至球凰,卻和暖如春狮腿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背呕诉。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工缘厢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人甩挫。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓贴硫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親伊者。 傳聞我的和親對象是個殘疾皇子英遭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評論 2 348