本文來自于公眾號鏈接: 徹底掌握CORS跨源資源共享
本文接上篇公眾號文章:徹底理解瀏覽器同源策略SOP
一.概述
在云時代,各種SAAS應(yīng)用層出不窮,各種互聯(lián)網(wǎng)API接口越來越豐富硕舆,H5技術(shù)在微信小程序球凰、支付寶小程序来颤、Hybird中大行其道,所有的這些都離不開跨源訪問德崭。
CORS即跨源資源共享(Cross-Origin Resource Sharing)斥黑,是由W3C組織維護(hù)的處于穩(wěn)定狀態(tài)的瀏覽器跨源訪問規(guī)范接癌,被現(xiàn)代主流版本瀏覽器充分支持缨叫。在普通的web應(yīng)用跨源訪問server的場景下,CORS是最優(yōu)的跨源訪問方案有咨。對比其他的方案座享,如iframe標(biāo)簽嵌套方案不夠安全婉商,JSONP方案功也只支持GET方法,CORS的安全性高且功能完善渣叛。
什么是跨源訪問
現(xiàn)代瀏覽器都支持同源策略SOP丈秩。假如有網(wǎng)站:
A的web頁調(diào)用B的資源,此時因?yàn)锳和B的源不同淳衙,就發(fā)生了跨源訪問蘑秽。根據(jù)SOP規(guī)范饺著,在默認(rèn)情況下A的web頁是無法訪問到B的資源的。CORS在盡量保證安全的前提下肠牲,放寬了SOP限制幼衰,使得瀏覽器可以跨源訪問服務(wù)器資源。
關(guān)于SOP細(xì)則缀雳,請參考上篇公眾號文章:徹底理解瀏覽器同源策略SOP
插圖:corsflow
除了最常用的XMLHttpRequest(AJAX)或Fetch發(fā)起的跨源請求渡嚣,web元素如Form表單、跨源加載css肥印、web字體严拒、甚至3D圖形引擎webGL跨源加載紋理等等都可以發(fā)起跨源請求。
本文通過Form表單和AJAX跨源訪問代碼示例來講解CORS的來龍去脈竖独。雖然代碼示例是基于Java技術(shù)棧的Spring Boot+Spring Security框架,但是CORS流程原理是多語言棧通用的挤牛,因此不影響其他語言棧的同學(xué)閱讀莹痢。
一.Form默認(rèn)支持跨源訪問
Form實(shí)際上是默認(rèn)支持跨源訪問的,CORS在發(fā)展過程中一直在努力保持不與Form沖突墓赴,這個問題常常被開發(fā)者忽略竞膳。
Form是從HTTP1.0就存在的歷史遺留技術(shù),至今仍然被廣泛應(yīng)用诫硕。Form沒有AJAX功能強(qiáng)大坦辟,產(chǎn)生危害的可能性更小,支持的Http方法只有GET和POST章办,支持的Http頭很少而且還無法自定義擴(kuò)展锉走。其次,F(xiàn)orm請求成功后是全頁面刷新的藕届,當(dāng)使用Form提交到其他地址挪蹭,原頁面會重定向到其他地址,使得腳本無法獲取新頁面中的內(nèi)容做惡意篡改休偶。瀏覽器有足夠的理由認(rèn)為即使Form跨源訪問也是安全的梁厉。
我們以Java程序?yàn)槔w驗(yàn)下Form默認(rèn)支持的跨源訪問踏兜。
新建一個Spring Boot工程词顾,命名為“back-end-spring-boot",端口號為8080碱妆,依賴spring-boot-starter-web包肉盹,新增一個controller接口:
@RestController
public class SampleController {
@GetMapping("/sample")
public String getSample() {
return "getSample";
}
@PostMapping("/sample")
public String post(HttpServletRequest request) {
return "post";
}
@DeleteMapping("/sample")
public String delete(String sampleId) {
return "delete";
}
@PutMapping("/sample")
public String put(HttpServletRequest request) {
return "put";
}
}
SampleController提供了基礎(chǔ)的GET,POST山橄,PUT垮媒,DELETE方法類型的接口舍悯。
在工程目錄“resources/static/”下新建一個html5文件index.html,添加一個Form表單:
<form action="http://localhost:8080/sample" method="POST" >
<div >
<label for="name">Enter your name: </label>
<input type="text" name="name" id="name" required>
</div>
<div >
<label for="name">Enter your password: </label>
<input type="text" name="password" id="password" required>
</div>
<div>
<input type="submit" value="提交">
</div>
</form>
運(yùn)行
- 運(yùn)行工程“back-end-spring-boot"
- 雙擊index.html睡雇,使用瀏覽器的file協(xié)議打開頁面萌衬。此時index.html和“back-end-spring-boot"是不同的源,可以模擬跨源訪問它抱。
- 點(diǎn)擊index.html的“提交”按鈕秕豫。
插圖;form
瀏覽器成功重定向到:http://localhost:8080/sample 观蓄,并且頁面顯示字符串“post”混移。
插圖;formresult侮穿。
此示例演示了工程“back-end-spring-boot"中并沒有任何跨源策略歌径,F(xiàn)orm就可以進(jìn)行跨源訪問。
二.AJAX使用CORS跨源訪問
與Form不同亲茅,XMLHttpRequest(AJAX)功能更靈活更強(qiáng)大回铛,也更容易帶來安全風(fēng)風(fēng)險。AJAX可以實(shí)現(xiàn)web的局部刷新克锣,并且每次請求可以直接讀取響應(yīng)內(nèi)容茵肃,瀏覽器認(rèn)為AJAX的跨源訪問是危險的。
1.跨源訪問錯誤
使用AJAX無法直接跨源訪問server袭祟。
在index.xml增加一個模擬AJAX請求的按鈕:
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script language="javascript" type="text/javascript">
function getSample() {
$.ajax({
type: 'GET',
url: 'http://localhost:8080/sample',
success: function (result) {
alert(result);
},
error: function (error, msg) {
alert(msg);
}
});
}
</script>
<button onclick="getSample()">getSample</button>
雙擊index.html验残,使用瀏覽器查看,此時點(diǎn)擊getSample按鈕巾乳,在瀏覽器控制臺會提示一個跨源訪問錯誤:
圖片:withoutCORS
在同源策略限制下您没,瀏覽器雖然可以訪問到server端,server端也會正常返回數(shù)據(jù)胆绊,但是返回的數(shù)據(jù)會被瀏覽器攔截紊婉,web應(yīng)用只能收到一個跨源訪問錯誤信息。
2.使用CORS跨源訪問
如果AJAX想跨源訪問server辑舷,需要在server端配置CORS規(guī)則喻犁。
server端工程“back-end-spring-boot”支持CORS,
首先引入Spring Security包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
再增加security配置類:
@EnableWebSecurity(debug = true)
@Profile("defaultCORS")
public class SecurityConfigDefaultCORS extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().permitAll();
http.cors();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
}
Spring Boot框架的 org.springframework.web.filter.CorsFilter 過濾器實(shí)現(xiàn)了CORS功能何缓。如果又使用了Spring Security肢础,只需要調(diào)用http.cors(),框架就會自動構(gòu)造一個CorsFilter的實(shí)例。CorsConfigurationSource實(shí)例負(fù)責(zé)CORS具體規(guī)則的全局配置碌廓。其中“corsConfiguration.addAllowedOrigin("*");”表示允許所有源訪問传轰,實(shí)際是server端返回時候添加“Access-Control-Allow-Origin”響應(yīng)頭。
重新啟動工程“back-end-spring-boot”谷婆,再雙擊index.html慨蛙,在瀏覽器查看辽聊,點(diǎn)擊getSample按鈕,彈出框顯示:
插圖:ajaxcors
此時AJAX可以使用GET方法跨源訪問http://localhost:8080/sample接口期贫。
大部分Web Server只需要一個過濾器類就實(shí)現(xiàn)了CORS的處理邏輯跟匆。以Java技術(shù)棧的tomcat為例,它提供了 org.apache.catalina.filters.CorsFilter 來實(shí)現(xiàn)CORS功能通砍。對于不便修改源碼的項目玛臂,直接修改tomcat配置就可以快速支持CORS。
3.CORS的響應(yīng)頭Access-Control-Allow-Methods控制允許的Http方法
上個示例AJAX使用GET方法進(jìn)行了跨源訪問封孙。如果同時要支持POST方法跨源訪問迹冤,則修改server端的響應(yīng)頭Access-Control-Allow-Methods,來控制允許訪問的HTTP方法虎忌。如:
Access-Control-Allow-Methods: POST, GET, OPTIONS
表示允許瀏覽器使用 POST, GET 和 OPTIONS 方法發(fā)起請求泡徙。
在index.html增加:
<script language="javascript" type="text/javascript">
function postSample() {
var postParam = {
'username':"a",
'password':"a"
};
$.ajax({
type: 'POST',
url: 'http://localhost:8080/sample',
data:postParam,
success: function (result) {
alert(result);
},
error: function (error, msg) {
alert(msg);
}
});
}
</script>
<button onclick="postSample()">postSample</button>
雙擊index.html,在瀏覽器打開頁面后點(diǎn)擊"postSample"按鈕膜蠢,瀏覽器報錯:
插圖:cors錯誤
再security配置類的corsConfigurationSource方法中增加:
corsConfiguration.addAllowedMethod("*");
星號代表允許所有Http方法如GET锋勺,POST,OPTIONS狡蝶,DELETE等等。
并禁用csrf保護(hù)(csrf保護(hù)不是本篇重點(diǎn)贮勃,禁用后贪惹,可以減少不必要的干擾)
http.csrf().disable();
刷新index.html,在瀏覽器打開頁面后點(diǎn)擊"postSample"按鈕寂嘉,瀏覽器顯示:
插圖:postsuccess
此時就可以跨源訪問"postSample"接口了奏瞬。
4.CORS的響應(yīng)頭Access-Control-Allow-Headers控制允許攜帶的Http頭
除了可以控制允許的Http方法,在server端的響應(yīng)頭Access-Control-Allow-Headers可以控制跨源請求中允許攜帶的Http頭泉孩。如:
Access-Control-Allow-Headers: X-TEST, Y-TEST
表示跨源訪問時允許攜帶自定義頭X-TEST和Y-TEST
在index.html的postSample方法請求時候傳遞自定義頭:
headers: {
'X-TEST':"test header"
},
刷新index.html頁面,點(diǎn)擊postSample按鈕會報錯:
插圖:allowheader
再在security的配置類增加:
corsConfiguration.addAllowedHeader("*");
星號(“*”)表示的允許所有請求頭硼端。
再次訪問則提示成功,并且server端可以調(diào)用“request.getHeader("X-TEST")”獲取自定義的Http頭信息 寓搬。
5.CORS的其他響應(yīng)頭
CORS還定義了其他server端可配置的響應(yīng)頭珍昨,通過這些響應(yīng)頭可以細(xì)粒度地控制跨源訪問規(guī)則。具體的有:
- Access-Control-Allow-Origin:允許跨源訪問的源列表
- Access-Control-Allow-Methods:允許跨源訪問的Http方法
- Access-Control-Allow-Headers:允許跨源訪問時候攜帶的Http頭
- Access-Control-Max-Age:預(yù)檢請求緩存的時間
- Access-Control-Expose-Headers:server端允許客戶端訪問的響應(yīng)頭列表
- Access-Control-Allow-Credentials:允許瀏覽器攜帶如cookie等用戶驗(yàn)證信息
Access-Control-Expose-Headers
其中的Access-Control-Expose-Headers在前后端分離架構(gòu)中經(jīng)常會用到句喷,比如登錄成功后server端經(jīng)常通過Http響應(yīng)頭回傳給瀏覽器一個token,如:
Authorization: 71f0ed0aa56d480a81ce78eb8cc99605
并且設(shè)置:
corsConfiguration.addExposedHeader(HttpHeaders.AUTHORIZATION);
此時瀏覽器調(diào)用getResponseHeader()可以獲取到Authorization中的token值镣典,然后緩存這個token,表示用戶已經(jīng)登錄成功唾琼。
需要注意兄春,CORS安全性規(guī)則定義Access-Control-Expose-Headers不可以設(shè)置為星號“*”。
Access-Control-Allow-Credentials
其中的Access-Control-Allow-Credentials最常用的場景是瀏覽器跨源訪問時可以攜帶跨源的cookie锡溯,也就是攜帶用戶驗(yàn)證狀態(tài)赶舆。比如在單點(diǎn)退出場景下哑姚,server A退出時候同時,瀏覽器同時跨源調(diào)用server B的退出接口芜茵,此時配置:
corsConfiguration.setAllowCredentials(true);
并且AJAX調(diào)用時候配置:
$.ajax({
...
xhrFields: {
withCredentials: true
},
crossDomain: true,: true,
...
})
此時Server A跨源訪問此時Server B時叙量,可以攜帶著此時Server B的cookie,此時Server B就可以拿到這個cookie進(jìn)行退出操作了夕晓。
三.CORS流程原理
CORS定義了一系列Http頭宛乃,定義了瀏覽器和Server之間的交互流程,使得CORS實(shí)現(xiàn)瀏覽器可以安全地跨源訪問server蒸辆,并且server可以細(xì)粒度地控制跨源訪問規(guī)則征炼。
對于Http頭來說,除了server端的響應(yīng)頭之外躬贡,CORS還規(guī)定了瀏覽器端的請求頭:
- Origin:表示發(fā)起的跨源請求的源谆奥。如A跨源訪問B,則Origin的值是A的源(schema拂玻,host酸些,port),server端只有接收到攜帶Origin頭的請求時檐蚜,才會認(rèn)為這是一個跨源請求魄懂。
- Access-Control-Request-Method:用在預(yù)檢請求過程中,代表實(shí)際請求的Http方法類型
- Access-Control-Request-Headers:用在預(yù)檢請求過程中闯第,代表實(shí)際請求需要攜帶的自定義Http頭
CORS協(xié)議是由最初的access-control訪問提案逐漸演變過來的市栗,因此這些CORS都以“Access-Control-”為前綴。
對于跨源訪問流程來說咳短,CORS本來可以簡化為大致兩個步驟:
- 瀏覽器發(fā)起OPTIONS方法類型的預(yù)檢請求(Preflight)
- server端驗(yàn)證預(yù)檢請求通過后填帽,瀏覽器再發(fā)送真實(shí)請求
然而CORS為了同時兼容Form和AJAX、為了向下兼容HTTP協(xié)議和瀏覽器咙好,無法做到這么簡單篡腌。比如FORM表單也必須可以跨源訪問卻無法支持OPTIONS類型的預(yù)檢請求。
因此CORS將瀏覽器請求分為兩種:
- 簡單跨源請求(Simple Cross-Origin Request)
- 需要預(yù)檢的跨源請求(Cross-Origin Request with Preflight)
1.簡單跨源請求
簡單跨源請求主要是為了使Form表單等技術(shù)也支持跨源訪問勾效,使得CORS協(xié)議可以向上和向下兼容嘹悼。
同時滿足“簡單HTTP方法”、“簡單HTTP頭”的請求层宫,可視為“簡單請求”绘迁。
簡單HTTP方法(Simple Method)
- GET
- HEAD
- POST
GET,POST卒密,HEAD方法是HTTP1.0協(xié)議就定義的缀台,F(xiàn)orm也只支持GET和POST。
簡單HTTP頭(Simple Header)
- Accept
- Accept-Language
- Content-Language
- Content-Type取值為:
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
注意POST跨源請求時哮奇,如果Content-Type不是上述的三種膛腐,則不是簡單跨源請求睛约。
無論是“簡單HTTP方法”還是“簡單HTTP頭”都與form表單支持的功能吻合。
2.需要預(yù)檢的跨源請求
除了簡單跨源請求之外的其他跨源請求哲身,不需要被form表單支持辩涝,所以跨源訪問時候不用再考慮對form表單做特殊地兼容。
CORS規(guī)定在實(shí)際跨源請求前勘天,需要先執(zhí)行預(yù)檢請求(Preflight)怔揩。預(yù)檢請求使用的是Http方法是OPTIONS,只有server端處理預(yù)檢請求通過后脯丝,瀏覽器才可以接著發(fā)送實(shí)際請求商膊。
插圖:preflightProccess
CORS之所以使用預(yù)檢請求,是因?yàn)轭A(yù)檢請求杜絕了不安全server被跨源訪問的可能性宠进。對server端來說晕拆,一旦能正確響應(yīng)預(yù)檢請求,就說明server是理解CORS且針對CORS做過專門處理材蹬。如果沒有preflight而是直接依靠服務(wù)器的響應(yīng)來確定請求是否正確实幕,從而使瀏覽器不必為單個調(diào)用發(fā)出兩個請求。這樣貌似簡化了操作堤器,但實(shí)際上相當(dāng)于假定服務(wù)器首先正確地驗(yàn)證了請求昆庇,然而實(shí)際上許多服務(wù)器沒有這樣做,這就是CSRF攻擊大行其道的原因闸溃。沒有preflight請求的話就會有不安全server存在的可能性整吆。
點(diǎn)擊postSample按鈕,查看瀏覽器控制臺:
插圖:prefight
CORS是個“圓滑”的協(xié)議圈暗,向下和向后的兼容性很好,假如有一天HTTP協(xié)議正式告別了歷史遺留的FORM裕膀,那么CORS協(xié)議不需要做大改動员串,只需讓瀏覽器把簡單請求的概念合并為復(fù)雜請求就可以了,CORS將會進(jìn)化得更加簡潔和高效昼扛。
四.最佳配置實(shí)踐
1.全局配置與接口單獨(dú)配置
一般實(shí)現(xiàn)的server端技術(shù)都可以對CORS進(jìn)行全局配置和每個接口單獨(dú)配置寸齐。全局配置便捷方便,一次配置抄谐,所有接口都生效渺鹦。每個接口單獨(dú)配置則可以精確地配置每個接口的跨源訪問規(guī)則。實(shí)際項目上建議采用全局配置和每個接口單獨(dú)配置相結(jié)合的方式來實(shí)踐蛹含。
以Java技術(shù)棧的SpringBoot+SpringSecurity框架為例毅厚。一般采用全局配置CorsConfigurationSource和接口級細(xì)粒度配置@CrossOrigin結(jié)合的方式,最終生效的屬性值是全局和細(xì)粒度配置合并后的值浦箱,需要注意Spring提供的默認(rèn)合并規(guī)則是“采用最大匹配進(jìn)行合并”吸耿,具體可以參考:org.springframework.web.cors.CorsConfiguration類的combine(CorsConfiguration)方法祠锣,
如:
多值屬性Access-Control-Allow-Headers,當(dāng)全局配置CorsConfigurationSource為:
corsConfiguration.addAllowedHeader("Authorization");
而接口單獨(dú)配置為:
@CrossOrigin(allowedHeaders = "*")
則最終生效的是星號“*”咽安。
而對于單值屬性Access-Control-Allow-Credentials伴网,則以@CrossOrigin為準(zhǔn)。
Access-Control-Allow-Credentials要尤其要注意妆棒,因?yàn)橐坏╅_啟就相當(dāng)于暴露了用戶相關(guān)信息澡腾,比如用戶的cookie和csrf token。一般全局配置將Access-Control-Allow-Credentials設(shè)置為false糕珊。在需要跨源傳遞用戶狀態(tài)的接口單獨(dú)配置為true动分。
2.其他
Access-Control-Allow-Origin等支持星號的響應(yīng)頭在生產(chǎn)環(huán)境不要使用星號“*”,要根據(jù)實(shí)際跨源訪問需求配置真實(shí)的列表放接。
本位的源碼上傳到了Github刺啦,地址:https://github.com/andyzhaozhao/spring-security-sample-cors
技術(shù)是不斷發(fā)展的,要用動態(tài)的眼光看待技術(shù)問題纠脾。能夠和技術(shù)不斷成長本身時間很美妙的事情
如果有任何問題和建議玛瘸,可以右下角點(diǎn)贊后評論,我們會第一時間回復(fù)苟蹈。
五.參考
更多干貨都在《spring security實(shí)戰(zhàn)》
官方資料
- CORS協(xié)議
- 關(guān)于domain的定義hosts-(domains-and-ip-addresses)
- 當(dāng)acac設(shè)置為true時候的安全問題
其他參考
- 跨域常見問題總結(jié)
- Authoritative guide to CORS (Cross-Origin Resource Sharing) for REST APIs
- 預(yù)檢請求
- Spring Web MVC CORS
- Spring Boot 2中對于CORS跨域訪問的快速支持
- HTTP之HEAD請求
- Jquery Ajax設(shè)置withCredentials解決跨域請求
- Web API 入門指南
- CORS:跨域資源共享 W3C的CORS Specification
- 瀏覽器的跨域問題以及解決方案
- Using CORS
- 跨域問題
- 怎樣與 CORS 和 cookie 打交道
- CORS 進(jìn)階之 Preflight 請求
- The Web Origin Concept
關(guān)于schema和protocal描述的是一個東西
- RFC 7230, section 2.7: Uniform Resource Identifiers
- URI scheme vs protocol
- Identifying resources on the Web
- URL representation
關(guān)于Orign定義
- HTML Living Standard
The definition of 'origin' in that specification. - Http Header Origin
- Http Hader origin協(xié)議
本為官方出處微信公眾號: 碼聞