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

背景

在最近的項目中,遇到這樣一個場景:合作方開發H5頁面並部署在合作方的服務器上,但頁面中嵌入了我方的SDK,SDK會直接調用我方的接口,如下圖:
在這裏插入圖片描述
但是控制檯中卻會收到如下報錯:

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.

這就是跨域的報錯。

跨域是什麼

跨域,是指瀏覽器不能執行其它網站的腳本。它是由瀏覽器的同源策略造成的,是瀏覽器對javascript實施的安全限制。

簡單來講,就是從地址A加載的頁面,不能訪問地址B的服務(如上圖)。此時地址A與地址B不同源。

所謂同源,就是域名、協議、端口均相同。舉個例子:

http://www.123.com/index.html 調用 http://www.123.com/abc.do (非跨域)
http://www.123.com/index.html 調用 http://www.456.com/abc.do (主域名不同:123/456,跨域)
http://abc.123.com/index.html 調用 http://def.123.com/server.do (子域名不同:abc/def,跨域)
http://www.123.com:8080/index.html 調用 http://www.123.com:8081/server.do(端口不同:8080/8081,跨域)
http://www.123.com/index.html 調用 https://www.123.com/server.do (協議不同:http/https,跨域)

如上所述,由於合作方的域名與我方的域名不同,從合作方加載的頁面,調用我方接口的時候,就會出現跨域的報錯。

是否有辦法可以解決這個問題呢,需要從CORS說起。

CORS

隨着互聯網的發展,同源策略嚴重影響了項目之間的連接,尤其是大項目,需要多個域名配合完成,因此W3C推出了CORS,即Cross-origin resource sharing(跨來源資源共享)。CORS的基本思想就是使用額外的HTTP頭部讓瀏覽器與服務器進行溝通,從而決定是否接受跨域請求。

CORS需要瀏覽器和服務器同時支持,目前,所有瀏覽器都支持該功能。對於開發者來說,CORS通信與同源的AJAX通信沒有區別,代碼完全一樣。瀏覽器在跨域訪問時,會自動添加HTTP頭信息,或者發起預檢請求,用戶對此毫無感知。因此是否支持跨域請求,關鍵在於服務器是否做了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,因此屬於非簡單請求。

對於這兩種請求,瀏覽器的處理方式是不一樣的。

簡單請求

對於簡單請求,瀏覽器採用先請求後判斷的方式,即瀏覽器直接發出CORS請求,即在請求頭中增加Origin字段,如圖:
在這裏插入圖片描述
Origin字段用來向服務器說明,本次請求來自於哪個源(協議+域名+端口),服務器決定是否允許這個源的訪問。

服務器判斷該源如果不在自己允許的範圍內,就返回一個正常的HTTP響應。瀏覽器判斷響應頭中是否包含Access-Control-Allow-Origin字段,如果沒有,瀏覽器就知道服務器是不允許跨域訪問的,就會拋出錯誤。

如果Origin在服務器允許的範圍內,服務器的HTTP響應中,就會包含如下字段:
在這裏插入圖片描述
Access-Control-Allow-Origin
它的值要麼是請求時Origin字段的值,要麼是一個*(表示接受任意域名的請求)。
Access-Control-Allow-Credentials
它的值是一個布爾值,表示是否允許發送Cookie。默認情況下,Cookie不包括在CORS請求之中。設爲true,即表示服務器明確許可,Cookie可以包含在請求中,一起發給服務器。
Access-Control-Allow-Headers
允許瀏覽器在CORS中發送的頭信息。
Access-Control-Allow-Methods
允許瀏覽器在CORS中使用的方法。

瀏覽器收到服務器返回的HTTP響應後,即可知道什麼樣的CORS請求是被允許的。

非簡單請求

對於非簡單請求,瀏覽器採用預檢請求,詢問服務器是否支持跨域請求。在正式的請求之前,瀏覽器會預先發送一個額外的OPTIONS請求,詢問服務器當前網頁所在的域名是否在服務器的許可名單之中,以及可以使用哪些HTTP方法和頭字段。只有得到肯定答覆,瀏覽器纔會發出正式的XMLHttpRequest請求,否則就報錯。如圖:
在這裏插入圖片描述
HTTP正式請求的方法是POST,並且發送一個頭信息content-type(本例中使用content-type=application/json,因此是非簡單請求)。

服務器收到預檢請求之後,檢查Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段,並做出響應,如下圖:
在這裏插入圖片描述
Access-Control-Max-Age
用來指定本次預檢請求的有效期,單位爲秒。上面結果中,有效期是3600秒,即允許緩存該條迴應3600秒,在此期間,可直接發送正式請求,不用再發預檢請求。

在上圖例中,瀏覽器請求Origin是http://192.168.47.130,服務器響應Access-Control-Allow-Origin是http://127.0.0.1,因此瀏覽器會報錯。只有在服務器響應與瀏覽器的請求內容相匹配,瀏覽器纔不報錯。

跨域的解決辦法

遇到跨域的報錯,可以分別從客戶端和服務端去解決。

客戶端

通過上面的分析可以知道,跨域的判斷是在瀏覽器進行的,服務器只是根據客戶端的請求做出正常的響應,服務端不對跨域做任何判斷。因此如果禁用了瀏覽器的跨域檢查,使瀏覽器不再對比Origin是否被服務器允許,即可發出正常的請求。

該方式需要所有客戶都修改瀏覽器的設置,顯然是不現實的,因此只在開發調試的過程中使用,如給chrome瀏覽器設置–disable-web-security參數。

服務端

服務端又有兩種解決方式:代理轉發和配置CORS。

代理轉發

代理轉發的架構如下:
在這裏插入圖片描述
增加代理服務器,和H5資源服務器放在同一個域名下,接口請求全走代理服務器,這樣就變成了同源訪問,不存在跨域訪問,因此就不會存在跨域的問題。

該方式中,所有發往目標服務器的數據,都會經過代理服務器,適用於同一個公司內部不同域名之間相互訪問的情況。但對於我們這個項目,由SDK發往我方服務器的數據是敏感數據,需客戶端直接發往我方服務器上,不能由合作方做代理轉發,因此不能使用此種方式。

使用此方式還需注意一點,應關注代理服務器的性能,代理服務器的性能應與後端的目標服務器的性能相匹配,否則代理服務器會成爲整個系統的性能瓶頸。

配置CORS

在目標服務器上配置CORS響應頭,這樣瀏覽器經過對比判斷之後,就可以發起正常的訪問。

目標服務一般是由軟負載和應用服務組成(如常見的apache+jboss,nginx+tomcat等組合),在軟負載和應用上都可添加CORS響應頭。

如在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;
    }
} 

此方式的優點是不用修改應用代碼,缺點是不能做細粒度的編程,從而做到細粒度的控制,如根據請求參數的不同而返回不同的結果。
另一種方式,就是修改應用代碼。通常是在服務器代碼中增加filter,在filter中在HTTP響應頭添加相應的字段,如下:

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);
    }

由於是通過代碼控制,因此可以實現細粒度的控制,在解決跨域問題的同時,可以滿足複雜的業務需求。

總結

  • 跨域是由瀏覽器的同源策略造成的,所謂同源,即域名、協議、端口均相同。
  • CORS(跨來源資源共享),通過添加HTTP頭信息,使瀏覽器判斷是否可以發起跨域訪問。
  • 瀏覽器將跨域請求分爲兩類:簡單請求和非簡單請求。簡單請求採取先請求後判斷的方式,非簡單請求採取預檢請求的方式判斷是否允許跨域訪問。
  • 解決跨域通常採用服務端代理轉發和配置CORS兩種方式。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章