本文來自於公眾號鏈接: 徹底掌握CORS跨源資源共享
)
本文接上篇公眾號文章:徹底理解瀏覽器同源策略SOP
一.概述
在雲時代,各種SAAS應用層出不窮,各種互聯網API接口越來越豐富,H5技術在微信小程序、支付寶小程序、Hybird中大行其道,所有的這些都離不開跨源訪問。
CORS即跨源資源共享(Cross-Origin Resource Sharing),是由W3C組織維護的處於穩定狀態的瀏覽器跨源訪問規范,被現代主流版本瀏覽器充分支持。在普通的web應用跨源訪問server的場景下,CORS是最優的跨源訪問方案。對比其他的方案,如iframe標簽嵌套方案不夠安全,JSONP方案功也只支持GET方法,CORS的安全性高且功能完善。
什么是跨源訪問
現代瀏覽器都支持同源策略SOP。假如有網站:
A的web頁調用B的資源,此時因為A和B的源不同,就發生了跨源訪問。根據SOP規范,在默認情況下A的web頁是無法訪問到B的資源的。CORS在盡量保證安全的前提下,放寬了SOP限制,使得瀏覽器可以跨源訪問服務器資源。
關於SOP細則,請參考上篇公眾號文章:徹底理解瀏覽器同源策略SOP
插圖:corsflow
除了最常用的XMLHttpRequest(AJAX)或Fetch發起的跨源請求,web元素如Form表單、跨源加載css、web字體、甚至3D圖形引擎webGL跨源加載紋理等等都可以發起跨源請求。
本文通過Form表單和AJAX跨源訪問代碼示例來講解CORS的來龍去脈。雖然代碼示例是基於Java技術棧的Spring Boot+Spring Security框架,但是CORS流程原理是多語言棧通用的,因此不影響其他語言棧的同學閱讀。
一.Form默認支持跨源訪問
Form實際上是默認支持跨源訪問的,CORS在發展過程中一直在努力保持不與Form沖突,這個問題常常被開發者忽略。
Form是從HTTP1.0就存在的歷史遺留技術,至今仍然被廣泛應用。Form沒有AJAX功能強大,產生危害的可能性更小,支持的Http方法只有GET和POST,支持的Http頭很少而且還無法自定義擴展。其次,Form請求成功后是全頁面刷新的,當使用Form提交到其他地址,原頁面會重定向到其他地址,使得腳本無法獲取新頁面中的內容做惡意篡改。瀏覽器有足夠的理由認為即使Form跨源訪問也是安全的。
我們以Java程序為例,體驗下Form默認支持的跨源訪問。
新建一個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提供了基礎的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>
運行
- 運行工程“back-end-spring-boot"
- 雙擊index.html,使用瀏覽器的file協議打開頁面。此時index.html和“back-end-spring-boot"是不同的源,可以模擬跨源訪問。
- 點擊index.html的“提交”按鈕。
插圖;form
瀏覽器成功重定向到:http://localhost:8080/sample ,並且頁面顯示字符串“post”。
插圖;formresult。
此示例演示了工程“back-end-spring-boot"中並沒有任何跨源策略,Form就可以進行跨源訪問。
二.AJAX使用CORS跨源訪問
與Form不同,XMLHttpRequest(AJAX)功能更靈活更強大,也更容易帶來安全風風險。AJAX可以實現web的局部刷新,並且每次請求可以直接讀取響應內容,瀏覽器認為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,使用瀏覽器查看,此時點擊getSample按鈕,在瀏覽器控制台會提示一個跨源訪問錯誤:
圖片:withoutCORS
在同源策略限制下,瀏覽器雖然可以訪問到server端,server端也會正常返回數據,但是返回的數據會被瀏覽器攔截,web應用只能收到一個跨源訪問錯誤信息。
2.使用CORS跨源訪問
如果AJAX想跨源訪問server,需要在server端配置CORS規則。
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 過濾器實現了CORS功能。如果又使用了Spring Security,只需要調用http.cors(),框架就會自動構造一個CorsFilter的實例。CorsConfigurationSource實例負責CORS具體規則的全局配置。其中“corsConfiguration.addAllowedOrigin("*");”表示允許所有源訪問,實際是server端返回時候添加“Access-Control-Allow-Origin”響應頭。
重新啟動工程“back-end-spring-boot”,再雙擊index.html,在瀏覽器查看,點擊getSample按鈕,彈出框顯示:
插圖:ajaxcors
此時AJAX可以使用GET方法跨源訪問http://localhost:8080/sample接口。
大部分Web Server只需要一個過濾器類就實現了CORS的處理邏輯。以Java技術棧的tomcat為例,它提供了 org.apache.catalina.filters.CorsFilter 來實現CORS功能。對於不便修改源碼的項目,直接修改tomcat配置就可以快速支持CORS。
3.CORS的響應頭Access-Control-Allow-Methods控制允許的Http方法
上個示例AJAX使用GET方法進行了跨源訪問。如果同時要支持POST方法跨源訪問,則修改server端的響應頭Access-Control-Allow-Methods,來控制允許訪問的HTTP方法。如:
Access-Control-Allow-Methods: POST, GET, OPTIONS
表示允許瀏覽器使用 POST, GET 和 OPTIONS 方法發起請求。
在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,在瀏覽器打開頁面后點擊"postSample"按鈕,瀏覽器報錯:
插圖:cors錯誤
再security配置類的corsConfigurationSource方法中增加:
corsConfiguration.addAllowedMethod("*");
星號代表允許所有Http方法如GET,POST,OPTIONS,DELETE等等。
並禁用csrf保護(csrf保護不是本篇重點,禁用后,可以減少不必要的干擾)
http.csrf().disable();
刷新index.html,在瀏覽器打開頁面后點擊"postSample"按鈕,瀏覽器顯示:
插圖:postsuccess
此時就可以跨源訪問"postSample"接口了。
4.CORS的響應頭Access-Control-Allow-Headers控制允許攜帶的Http頭
除了可以控制允許的Http方法,在server端的響應頭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頁面,點擊postSample按鈕會報錯:
插圖:allowheader
再在security的配置類增加:
corsConfiguration.addAllowedHeader("*");
星號(“*”)表示的允許所有請求頭。
再次訪問則提示成功,並且server端可以調用“request.getHeader("X-TEST")”獲取自定義的Http頭信息 。
5.CORS的其他響應頭
CORS還定義了其他server端可配置的響應頭,通過這些響應頭可以細粒度地控制跨源訪問規則。具體的有:
- Access-Control-Allow-Origin:允許跨源訪問的源列表
- Access-Control-Allow-Methods:允許跨源訪問的Http方法
- Access-Control-Allow-Headers:允許跨源訪問時候攜帶的Http頭
- Access-Control-Max-Age:預檢請求緩存的時間
- Access-Control-Expose-Headers:server端允許客戶端訪問的響應頭列表
- Access-Control-Allow-Credentials:允許瀏覽器攜帶如cookie等用戶驗證信息
Access-Control-Expose-Headers
其中的Access-Control-Expose-Headers在前后端分離架構中經常會用到,比如登錄成功后server端經常通過Http響應頭回傳給瀏覽器一個token,如:
Authorization: 71f0ed0aa56d480a81ce78eb8cc99605
並且設置:
corsConfiguration.addExposedHeader(HttpHeaders.AUTHORIZATION);
此時瀏覽器調用getResponseHeader()可以獲取到Authorization中的token值,然后緩存這個token,表示用戶已經登錄成功。
需要注意,CORS安全性規則定義Access-Control-Expose-Headers不可以設置為星號“*”。
Access-Control-Allow-Credentials
其中的Access-Control-Allow-Credentials最常用的場景是瀏覽器跨源訪問時可以攜帶跨源的cookie,也就是攜帶用戶驗證狀態。比如在單點退出場景下,server A退出時候同時,瀏覽器同時跨源調用server B的退出接口,此時配置:
corsConfiguration.setAllowCredentials(true);
並且AJAX調用時候配置:
$.ajax({
...
xhrFields: {
withCredentials: true
},
crossDomain: true,: true,
...
})
此時Server A跨源訪問此時Server B時,可以攜帶着此時Server B的cookie,此時Server B就可以拿到這個cookie進行退出操作了。
三.CORS流程原理
CORS定義了一系列Http頭,定義了瀏覽器和Server之間的交互流程,使得CORS實現瀏覽器可以安全地跨源訪問server,並且server可以細粒度地控制跨源訪問規則。
對於Http頭來說,除了server端的響應頭之外,CORS還規定了瀏覽器端的請求頭:
- Origin:表示發起的跨源請求的源。如A跨源訪問B,則Origin的值是A的源(schema,host,port),server端只有接收到攜帶Origin頭的請求時,才會認為這是一個跨源請求。
- Access-Control-Request-Method:用在預檢請求過程中,代表實際請求的Http方法類型
- Access-Control-Request-Headers:用在預檢請求過程中,代表實際請求需要攜帶的自定義Http頭
CORS協議是由最初的access-control訪問提案逐漸演變過來的,因此這些CORS都以“Access-Control-”為前綴。
對於跨源訪問流程來說,CORS本來可以簡化為大致兩個步驟:
- 瀏覽器發起OPTIONS方法類型的預檢請求(Preflight)
- server端驗證預檢請求通過后,瀏覽器再發送真實請求
然而CORS為了同時兼容Form和AJAX、為了向下兼容HTTP協議和瀏覽器,無法做到這么簡單。比如FORM表單也必須可以跨源訪問卻無法支持OPTIONS類型的預檢請求。
因此CORS將瀏覽器請求分為兩種:
- 簡單跨源請求(Simple Cross-Origin Request)
- 需要預檢的跨源請求(Cross-Origin Request with Preflight)
1.簡單跨源請求
簡單跨源請求主要是為了使Form表單等技術也支持跨源訪問,使得CORS協議可以向上和向下兼容。
同時滿足“簡單HTTP方法”、“簡單HTTP頭”的請求,可視為“簡單請求”。
簡單HTTP方法(Simple Method)
- GET
- HEAD
- POST
GET,POST,HEAD方法是HTTP1.0協議就定義的,Form也只支持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.需要預檢的跨源請求
除了簡單跨源請求之外的其他跨源請求,不需要被form表單支持,所以跨源訪問時候不用再考慮對form表單做特殊地兼容。
CORS規定在實際跨源請求前,需要先執行預檢請求(Preflight)。預檢請求使用的是Http方法是OPTIONS,只有server端處理預檢請求通過后,瀏覽器才可以接着發送實際請求。
插圖:preflightProccess
CORS之所以使用預檢請求,是因為預檢請求杜絕了不安全server被跨源訪問的可能性。對server端來說,一旦能正確響應預檢請求,就說明server是理解CORS且針對CORS做過專門處理。如果沒有preflight而是直接依靠服務器的響應來確定請求是否正確,從而使瀏覽器不必為單個調用發出兩個請求。這樣貌似簡化了操作,但實際上相當於假定服務器首先正確地驗證了請求,然而實際上許多服務器沒有這樣做,這就是CSRF攻擊大行其道的原因。沒有preflight請求的話就會有不安全server存在的可能性。
點擊postSample按鈕,查看瀏覽器控制台:
插圖:prefight
CORS是個“圓滑”的協議,向下和向后的兼容性很好,假如有一天HTTP協議正式告別了歷史遺留的FORM,那么CORS協議不需要做大改動,只需讓瀏覽器把簡單請求的概念合並為復雜請求就可以了,CORS將會進化得更加簡潔和高效。
四.最佳配置實踐
1.全局配置與接口單獨配置
一般實現的server端技術都可以對CORS進行全局配置和每個接口單獨配置。全局配置便捷方便,一次配置,所有接口都生效。每個接口單獨配置則可以精確地配置每個接口的跨源訪問規則。實際項目上建議采用全局配置和每個接口單獨配置相結合的方式來實踐。
以Java技術棧的SpringBoot+SpringSecurity框架為例。一般采用全局配置CorsConfigurationSource和接口級細粒度配置@CrossOrigin結合的方式,最終生效的屬性值是全局和細粒度配置合並后的值,需要注意Spring提供的默認合並規則是“采用最大匹配進行合並”,具體可以參考:org.springframework.web.cors.CorsConfiguration類的combine(CorsConfiguration)方法,
如:
多值屬性Access-Control-Allow-Headers,當全局配置CorsConfigurationSource為:
corsConfiguration.addAllowedHeader("Authorization");
而接口單獨配置為:
@CrossOrigin(allowedHeaders = "*")
則最終生效的是星號“*”。
而對於單值屬性Access-Control-Allow-Credentials,則以@CrossOrigin為准。
Access-Control-Allow-Credentials要尤其要注意,因為一旦開啟就相當於暴露了用戶相關信息,比如用戶的cookie和csrf token。一般全局配置將Access-Control-Allow-Credentials設置為false。在需要跨源傳遞用戶狀態的接口單獨配置為true。
2.其他
Access-Control-Allow-Origin等支持星號的響應頭在生產環境不要使用星號“*”,要根據實際跨源訪問需求配置真實的列表。
本位的源碼上傳到了Github,地址:https://github.com/andyzhaozhao/spring-security-sample-cors
技術是不斷發展的,要用動態的眼光看待技術問題。能夠和技術不斷成長本身時間很美妙的事情
如果有任何問題和建議,可以右下角點贊后評論,我們會第一時間回復。
五.參考
更多干貨都在《spring security實戰》
官方資料
其他參考
- 跨域常見問題總結
- Authoritative guide to CORS (Cross-Origin Resource Sharing) for REST APIs
- 預檢請求
- Spring Web MVC CORS
- Spring Boot 2中對於CORS跨域訪問的快速支持
- HTTP之HEAD請求
- Jquery Ajax設置withCredentials解決跨域請求
- Web API 入門指南
- CORS:跨域資源共享 W3C的CORS Specification
- 瀏覽器的跨域問題以及解決方案
- Using CORS
- 跨域問題
- 怎樣與 CORS 和 cookie 打交道
- CORS 進階之 Preflight 請求
- The Web Origin Concept
關於schema和protocal描述的是一個東西
- RFC 7230, section 2.7: Uniform Resource Identifiers
- URI scheme vs protocol
- Identifying resources on the Web
- URL representation
關於Orign定義
- HTML Living Standard
The definition of 'origin' in that specification. - Http Header Origin
- Http Hader origin協議
本為官方出處微信公眾號: 碼聞
