跨站資源共享CORS原理深度解析
跨站資源共享CORS原理深度解析
我相信如果你寫過前後端分離的web應用程式,或者寫過一些ajax請求呼叫,你可能會遇到過CORS錯誤。
- CORS是什麼?
- 它與安全性有關嗎?
- 為什麼要有CORS?它解決了什麼目的?
- CORS是怎樣執行的?
如果您有這些問題,那麼這篇文章非常適合您。
一、什麼是CORS?
要了解什麼是CORS(Cross-Origin Resource Sharing:跨站資源共享),首先我們需要了解什麼是同源策略Same Origin Policy
(SOP)。SOP是所有的現代瀏覽器都具備的安全措施,它不允許從一個載入的js指令碼和資源的Origin域與另一個Origin域進行互動。換句話說,如果您的網站是www.example.com
www.test.com
發出XHR請求。
那麼SOP有什麼用?如果沒有同源策略的限制,你想想會發生什麼?比如:您已經登入到微博,並且不小心打開了一個惡意網站。該網站可以向微博發出請求,並從您微博登入的會話中提取個人資訊。這顯然是巨大的安全問題,為了防止這種情況,在瀏覽器中實施同源策略的限制。實際上,伺服器並沒有意識到在瀏覽器端發生的這一切,您仍然可以使用curl或postman發出相同的請求,並且一切響應正常,因為這些工具上沒有SOP。
如果說SOP是限制跨源訪問的一種方式,那麼CORS是一種繞過SOP限制並允許您的前端向伺服器提出合法請求的方法。如果您的服務端的確是存在跨域的情況(實際上對於現代分散式應用,這很常見),由於SOP限制您的客戶端將無法向多節點跨域伺服器發出xhr請求。救星就出現了,CORS使我們能夠以安全且可管理的方式做到跨域請求,突破同源策略的限制。
二、同源策略的源(Same Origin Policy的Origin)
源由三部分組成:協議,hostip(域)和埠。例如
http://example.com/xxx/index.html
和http://example.com/yyy/index.html
是同源,http://example.com:80
和http://example.com
(對於http預設埠為80)是同源。- 由於協議不同,
http://example.com/app1
和https://example.com/app2
是不同的源。 http://example.com
,http://www.example.com
由於域名不同,也是不同的源- 非常要注意的是
http://localhost
http://127.0.0.1
是不同的源
同源策略就是:不允許不同的ip、埠、協議的應用在瀏覽器內進行互相資源共享、請求呼叫。
三、CORS如何運作?
CORS規範允許伺服器向瀏覽器返回一些HTTP Headers,瀏覽器可以基於這些HTTP Headers來決定是否突破SOP的限制。最主要的一個HTTP Headers是Access-Control-Allow-Origin。
//目標服務允許所有的網站對其進行跨域訪問
Access-Control-Allow-Origin: *
//目標服務允許特定的網站對其進行跨域訪問
Access-Control-Allow-Origin: https://example.com
CORS有兩種型別的請求:“simple”簡單請求和“preflight”預檢請求,根據請求方法的不同由瀏覽器確定使用哪種請求。
simple簡單請求:
如果符合以下所有條件,則API請求被視為簡單請求:
- API方法是以下方法之一:GET,POST或HEAD。
Content-Type
請求頭包含:application/x-www-form-urlencoded
,multipart/form-data
,text/plain
這兩個條件將構成大多數簡單請求的用例,但是可以在此處找到更詳細的簡單請求條件列表。
如果您的API請求被視為simple
簡單請求,這個請求就可以直接被髮送給伺服器。伺服器使用CORS HTTP Headers進行響應,瀏覽器將檢查Access-Control-Allow-Origin
後決定這個請求是否可以突破同源策略的限制,進行下一步的處理。
preflight預檢請求:
如果您的API請求不滿足成為簡單請求的標準(最常見不滿足簡單請求標準的Content-Type
值為application/json
),則瀏覽器將在傳送實際請求之前發出預檢請求。
舉一個例子,我們嘗試使用GET
請求https://example.com/status
,Content-Type
是application/json
,所以瀏覽器認為它不符合一個簡單請求的標準,因此瀏覽器會在發出實際請求之前發出預檢請求,這個預檢請求是使用HTTP的 OPTIONS方法發出的:
curl --location --request OPTIONS 'http://example.com/status' \
--header 'Access-Control-Request-Method: GET' \
--header 'Access-Control-Request-Headers: Content-Type, Accept' \
--header 'Origin: http://test.com'
上面的curl就是模擬預檢請求,實際作用是:瀏覽器希望告訴伺服器,我的實際請求將使用HTTPGET
method進行呼叫,Content-Type
與Accept
作為HTTP headers,這個請求是從https://test.com
發起的。伺服器響應此請求:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: OPTIONS, GET, HEAD, POST
Access-Control-Allow-Headers: Content-Type, Accept
Access-Control-Allow-Origin
:允許發出請求的源,或者*
可以從任何來源發出請求。(即允許跨域的源)Access-Control-Allow-Methods
:允許的以逗號分隔的HTTP方法列表。(即允許跨域的HTTP方法)Access-Control-Allow-Headers
:允許傳送的HTTP headers列表。
瀏覽器收到服務端的預檢請求響應之後,在我們的示例中伺服器響應*
可以從任何來源發出請求,因此現在瀏覽器將再次訪問https://example.com/status
,使用GET方法(不再是OPTIONS方法),瀏覽器將不再限制該請求的發出與響應資料的接收。
如果預檢請求響應的Origin是特定的Access-Control-Allow-Origin: http://domain.com
,瀏覽器將出現Cross-Origin Request Blocked
錯誤。因為伺服器端預檢結果只允許http://domain.com
發出跨域請求,不允許其他應用向我發出跨域請求。
四、如何處理CORS錯誤
我們現在知道什麼是CORS及其工作原理,後面的事情其實就簡單了。從上面的內容我們需要注意的是,對CORS的完全控制權在伺服器,即伺服器可以允許或禁止源的跨域訪問。所以說跨域問題的處理一般都在服務端進行,不同的服務端的處理HTTP 請求頭的程式碼是不一樣的,當然也可以不用寫程式碼,比如:nginx、haproxy設定。但是萬變不離其宗:最終都是對HTTP Headers進行重寫。
我就簡單的舉幾個例子:
比如Servlet處理跨域
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) resp;
response.setHeader("Access-Control-Allow-Origin", "*"); //解決跨域訪問報錯
response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
chain.doFilter(req, resp);
}
比如Spring MVC配置
@Configuration
public class GlobalCorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") //新增對映路徑,“/**”表示對所有的路徑實行全域性跨域訪問許可權的設定
.allowedOrigins("*") //開放哪些ip、埠、域名的訪問許可權
.allowCredentials(true) //是否允許傳送Cookie資訊
.allowedMethods("GET","POST", "PUT", "DELETE") //開放哪些Http方法,允許跨域訪問
.allowedHeaders("*") //允許HTTP請求中的攜帶哪些Header資訊
.exposedHeaders("*"); //暴露哪些頭部資訊(因為跨域訪問預設不能獲取全部頭部資訊)
}
};
}
}