1. 程式人生 > 實用技巧 >跨站資源共享CORS原理深度解析

跨站資源共享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.htmlhttp://example.com/yyy/index.html是同源,
  • http://example.com:80http://example.com(對於http預設埠為80)是同源。
  • 由於協議不同,http://example.com/app1https://example.com/app2是不同的源。
  • http://example.comhttp://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-urlencodedmultipart/form-datatext/plain

這兩個條件將構成大多數簡單請求的用例,但是可以在此處找到更詳細的簡單請求條件列表。

如果您的API請求被視為simple簡單請求,這個請求就可以直接被髮送給伺服器。伺服器使用CORS HTTP Headers進行響應,瀏覽器將檢查Access-Control-Allow-Origin後決定這個請求是否可以突破同源策略的限制,進行下一步的處理。

preflight預檢請求:

如果您的API請求不滿足成為簡單請求的標準(最常見不滿足簡單請求標準的Content-Type值為application/json),則瀏覽器將在傳送實際請求之前發出預檢請求。

舉一個例子,我們嘗試使用GET請求https://example.com/statusContent-Typeapplication/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就是模擬預檢請求,實際作用是:瀏覽器希望告訴伺服器,我的實際請求將使用HTTPGETmethod進行呼叫,Content-TypeAccept作為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("*");   //暴露哪些頭部資訊(因為跨域訪問預設不能獲取全部頭部資訊)
            }
        };
    }
}