CORS/JSONP比較

HideInTime發表於2020-11-24

CORS 跨域 實現思路及相關解決方案

本篇包括以下內容:

  • CORS 定義
  • CORS 對比 JSONP
  • CORS,BROWSER支援情況
  • 主要用途
  • Ajax請求跨域資源的異常
  • CORS 實現思路
  • 安全說明
  • CORS 幾種解決方案
    • 自定義CORSFilter
    • Nginx 配置支援Ajax跨域
    • 支援多域名配置的CORS Filter

keyword:cors,跨域,ajax,403,filter,RESTful,origin,http,nginx,jsonp

原創作品,轉載請附帶原文路徑:http://www.cnblogs.com/sloong/p/cors.html

CORS 定義#

Cross-Origin Resource Sharing(CORS)跨來源資源共享是一份瀏覽器技術的規範,提供了 Web 服務從不同域傳來沙盒指令碼的方法,以避開瀏覽器的同源策略,是 JSONP 模式的現代版。與 JSONP 不同,CORS 除了 GET 要求方法以外也支援其他的 HTTP 要求。用 CORS 可以讓網頁設計師用一般的 XMLHttpRequest,這種方式的錯誤處理比 JSONP 要來的好。另一方面,JSONP 可以在不支援 CORS 的老舊瀏覽器上運作。現代的瀏覽器都支援 CORS。

CORS是W3c工作草案,它定義了在跨域訪問資源時瀏覽器和伺服器之間如何通訊。CORS背後的基本思想是使用自定義的HTTP頭部允許瀏覽器和伺服器相互瞭解對方,從而決定請求或響應成功與否。W3C CORS 工作草案
同源策略:是瀏覽器最核心也最基本的安全功能;同源指的是:同協議,同域名和同埠。精髓:認為自任何站點裝載的信賴內容是不安全的。當被瀏覽器半信半疑的指令碼執行在沙箱時,它們應該只被允許訪問來自同一站點的資源,而不是那些來自其它站點可能懷有惡意的資源;參考:JavaScript 的同源策略
JSON & JSONP:JSON 是一種基於文字的資料交換方式,或者叫做資料描述格式。JSONP是資料格式JSON的一種“使用模式”,可以讓網頁從別的網域要資料,由於同源策略,一般來說位於server1.example.com的網頁無法與不是 server1.example.com的伺服器溝通,而HTML的script元素是一個例外。利用script元素的這個開放策略,網頁可以得到從其他來源動態產生的JSON資料,而這種使用模式就是所謂的JSONP

CORS 對比 JSONP#

都能解決 Ajax直接請求普通檔案存在跨域無許可權訪問的問題

  1. JSONP只能實現GET請求,而CORS支援所有型別的HTTP請求
  2. 使用CORS,開發者可以使用普通的XMLHttpRequest發起請求和獲得資料,比起JSONP有更好的錯誤處理
  3. JSONP主要被老的瀏覽器支援,它們往往不支援CORS,而絕大多數現代瀏覽器都已經支援了CORS

CORS,BROWSER支援情況#

資料來源:caniuse.com

IE6,IE7,Opera min 不支援CORS。具體可參看資料來源中的 'show all'

主要用途#

  • From a browser script perspective: By allowing cross-domain requests, which are subject to tighter controls on the types of data that is exchanged. Cookies, for instance, are blocked unless specifically requested by the XHR author and allowed by the cross-domain web service. This is done to mitigate the risk of data leaks.
  • From a web service perspective: By utilising the origin URL reported by the browser the target cross-domain web service can determine, based on its origin policy, whether to allow or deny the request.

Ajax請求跨域資源的異常#

當出現如下異常時,那麼就需要考慮跨域的問題了
例如 localhost:63343 通過Ajax請求http://192.168.10.61:8080伺服器資源時就會出現如下異常:

CORS 實現思路#

CORS背後的基本思想是使用自定義的HTTP頭部允許瀏覽器和伺服器相互瞭解對方,從而決定請求或響應成功與否

安全說明#

CORS is not about providing server-side security. The Origin request header is produced by the browser and the server has no direct means to verify it.

CORS 並不是為了解決服務端安全問題,而是為了解決如何跨域呼叫資源。至於如何設計出 安全的開放API,卻是另一個問題了,這裡提下一些思路:

  1. 請求時間有效性(驗證timestamp與服務接到請求的時間相差是否在指定範圍內,比如5分鐘內)
  2. token驗證
  3. ip驗證
  4. 來源驗證

例如

	{
		'name': 使用者名稱,
		‘key: 加密的驗證key,//(name+secret+timestamp來通過不可逆加密生成)
		‘timestamp’: 時間戳,//驗證timestamp與服務接到請求的時間相差是否在指定範圍內,比如5分鐘內
	}

CORS 幾種解決方案#

CORS背後的基本思想是使用自定義的HTTP頭部允許瀏覽器和伺服器相互瞭解對方,從而決定請求或響應成功與否.

Access-Control-Allow-Origin:指定授權訪問的域
Access-Control-Allow-Methods:授權請求的方法(GET, POST, PUT, DELETE,OPTIONS等)

一:簡單的自定義CORSFilter / Interceptor#

適合設定單一的(或全部)授權訪問域,所有配置都是固定的,特簡單。也沒根據請求的型別做不同的處理

在web.xml 中新增filter

<filter>
    <filter-name>cros</filter-name>
    <filter-class>cn.ifengkou.test.filter.CORSFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>cros</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

新增CORSFilter 類

@Component
public class CORSFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        response.addHeader("Access-Control-Allow-Origin", "*");
        response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
        response.addHeader("Access-Control-Allow-Headers", "Content-Type");
        response.addHeader("Access-Control-Max-Age", "1800");//30 min
        filterChain.doFilter(request, response);
    }
}

Access-Control-Allow-Origin只能配置 * 或者一個域名
比如配置了192.168.56.130,那麼只有192.168.56.130 能拿到資料,否則全部報403異常

response.addHeader("Access-Control-Allow-Origin", "http://192.168.56.130");

二:Nginx 配置支援Ajax跨域#

這裡是一個nginx啟用COSR的參考配置:來源

#
# Wide-open CORS config for nginx
#
location / {
     if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        #
        # Custom headers and headers various browsers *should* be OK with but aren't
        #
        add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
        #
        # Tell client that this pre-flight info is valid for 20 days
        #
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain charset=UTF-8';
        add_header 'Content-Length' 0;
        return 204;
     }
     if ($request_method = 'POST') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
     }
     if ($request_method = 'GET') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
     }
}

三:支援多域名配置的CORS Filter#

因為知道已經有可以用的庫可以解決,所以就沒重複造輪子了。其實因為懶,看看別人的原始碼算了。。。

mvnrepository搜尋cors-filter,目前也就兩個可以用

這兩個也都大同小異,因為ebay開源在github上,也有詳細的README,那麼就以ebay的cors-filter為例

配置#

新增依賴包到專案:

<dependency>
	<groupId>org.ebaysf.web</groupId>
	<artifactId>cors-filter</artifactId>
	<version>1.0.1</version>
</dependency>

新增配置(具體配置項,還是見專案的README.md吧)

  <filter>
    <filter-name>CORS Filter</filter-name>
    <filter-class>org.ebaysf.web.cors.CORSFilter</filter-class>
    <init-param>
      <param-name>cors.allowed.origins</param-name>
      <param-value>http://192.168.56.129,http://192.168.56.130</param-value>
    </init-param>
    <init-param>
      <param-name>cors.allowed.methods</param-name>
      <param-value>GET,POST,HEAD,OPTIONS,PUT</param-value>
    </init-param>
    <init-param>
      <param-name>cors.allowed.headers</param-name>
      <param-value>Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>CORS Filter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

原始碼分析#

原始碼地址:github。但通過IDEA Decompiled 出來的更清晰.....,以下是反編譯的

ebaysf的cors-filter 只有一個類CORSFilter。也就是一個攔截器,implements Filter

public final class CORSFilter implements Filter {

通過是實現Filter 的init 方法從配置檔案中讀取引數:

public void init(FilterConfig filterConfig) throws ServletException {
    this.parseAndStore("*", "GET,POST,HEAD,OPTIONS", "Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers", "", "true", "1800", "false", "true");
    this.filterConfig = filterConfig;
    this.loggingEnabled = false;
    if(filterConfig != null) {
        String configAllowedOrigins = filterConfig.getInitParameter("cors.allowed.origins");
        String configAllowedHttpMethods = filterConfig.getInitParameter("cors.allowed.methods");
        String configAllowedHttpHeaders = filterConfig.getInitParameter("cors.allowed.headers");
        String configExposedHeaders = filterConfig.getInitParameter("cors.exposed.headers");
        String configSupportsCredentials = filterConfig.getInitParameter("cors.support.credentials");
        String configPreflightMaxAge = filterConfig.getInitParameter("cors.preflight.maxage");
        String configLoggingEnabled = filterConfig.getInitParameter("cors.logging.enabled");
        String configDecorateRequest = filterConfig.getInitParameter("cors.request.decorate");
        this.parseAndStore(configAllowedOrigins, configAllowedHttpMethods, configAllowedHttpHeaders, configExposedHeaders, configSupportsCredentials, configPreflightMaxAge, configLoggingEnabled, configDecorateRequest);
    }
}

parseAndStore 方法,解析引數。以 解析cors.allowed.orgins為例;其他引數同理

	Set e;
    if(allowedOrigins != null) {
        if(allowedOrigins.trim().equals("*")) {
            this.anyOriginAllowed = true;
        } else {
            this.anyOriginAllowed = false;
            e = this.parseStringToSet(allowedOrigins);
            this.allowedOrigins.clear();
            this.allowedOrigins.addAll(e);
        }
    }
//parseStringToSet
//對多域名用點分割,加到HashSet中,再賦給allowedOrigins(Collection<String> allowedOrigins = new HashSet();)
private Set<String> parseStringToSet(String data) {
    String[] splits;
    if(data != null && data.length() > 0) {
        splits = data.split(",");
    } else {
        splits = new String[0];
    }

    HashSet set = new HashSet();
    if(splits.length > 0) {
        String[] arr$ = splits;
        int len$ = splits.length;

        for(int i$ = 0; i$ < len$; ++i$) {
            String split = arr$[i$];
            set.add(split.trim());
        }
    }

    return set;
}

如何實現 doFilter

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    if(servletRequest instanceof HttpServletRequest && servletResponse instanceof HttpServletResponse) {
        HttpServletRequest request1 = (HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse)servletResponse;
		//識別request 屬於哪種類別
        CORSFilter.CORSRequestType requestType = this.checkRequestType(request1);
        if(this.decorateRequest) {
            decorateCORSProperties(request1, requestType);
        }

        switch(CORSFilter.SyntheticClass_1.$SwitchMap$org$ebaysf$web$cors$CORSFilter$CORSRequestType[requestType.ordinal()]) {
        case 1:
            this.handleSimpleCORS(request1, response, filterChain);
            break;
        case 2:
            this.handleSimpleCORS(request1, response, filterChain);
            break;
        case 3:
            this.handlePreflightCORS(request1, response, filterChain);
            break;
        case 4:
            this.handleNonCORS(request1, response, filterChain);
            break;
        default:
            this.handleInvalidCORS(request1, response, filterChain);
        }

    } else {
        String request = "CORS doesn\'t support non-HTTP request or response.";
        throw new ServletException(request);
    }
}

判斷request類別,根據類別進行差異化處理。handleSimpleCORS 處理過程,判斷是否設定允許所有origin引數,判斷是否符合httpMethods要求,判斷此次request的origin(origin = request.getHeader("Origin"))是否在allowedOrigins(origin白名單)內。如果在,就設定response.addHeader("Access-Control-Allow-Origin", origin);這樣也就實現了多域名支援。流程圖就不畫了...

public void handleSimpleCORS(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        CORSFilter.CORSRequestType requestType = this.checkRequestType(request);
        String origin;
        if(requestType != CORSFilter.CORSRequestType.SIMPLE && requestType != CORSFilter.CORSRequestType.ACTUAL) {
            origin = "Expects a HttpServletRequest object of type " + CORSFilter.CORSRequestType.SIMPLE + " or " + CORSFilter.CORSRequestType.ACTUAL;
            throw new IllegalArgumentException(origin);
        } else {
            origin = request.getHeader("Origin");
            String method = request.getMethod();
            if(!this.isOriginAllowed(origin)) {
                this.handleInvalidCORS(request, response, filterChain);
            } else if(!this.allowedHttpMethods.contains(method)) {
                this.handleInvalidCORS(request, response, filterChain);
            } else {
                if(this.anyOriginAllowed && !this.supportsCredentials) {
                    response.addHeader("Access-Control-Allow-Origin", "*");
                } else {
                    response.addHeader("Access-Control-Allow-Origin", origin);
                }

                if(this.supportsCredentials) {
                    response.addHeader("Access-Control-Allow-Credentials", "true");
                }

                if(this.exposedHeaders != null && this.exposedHeaders.size() > 0) {
                    String exposedHeadersString = join(this.exposedHeaders, ",");
                    response.addHeader("Access-Control-Expose-Headers", exposedHeadersString);
                }

                filterChain.doFilter(request, response);
            }
        }
    }

為了避免對引數一知半解,就把作者的引數描述表貼上來,通過參數列可以瞭解下header裡面各個引數的作用

param-namedescription
cors.allowed.originsA list of origins that are allowed to access the resource. A '*' can be specified to enable access to resource from any origin. Otherwise, a whitelist of comma separated origins can be provided. Ex: http://www.w3.orghttps://www.apache.orgDefaults: * (Any origin is allowed to access the resource).
cors.allowed.methodsA comma separated list of HTTP methods that can be used to access the resource, using cross-origin requests. These are the methods which will also be included as part of 'Access-Control-Allow-Methods' header in a pre-flight response. Ex: GET,POST. Defaults: GET,POST,HEAD,OPTIONS
cors.allowed.headersA comma separated list of request headers that can be used when making an actual request. These header will also be returned as part of 'Access-Control-Allow-Headers' header in a pre-flight response. Ex: Origin,Accept. Defaults: Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers
cors.exposed.headersA comma separated list of headers other than the simple response headers that browsers are allowed to access. These are the headers which will also be included as part of 'Access-Control-Expose-Headers' header in the pre-flight response. Ex: X-CUSTOM-HEADER-PING,X-CUSTOM-HEADER-PONG. Default: None
cors.preflight.maxageThe amount of seconds, browser is allowed to cache the result of the pre-flight request. This will be included as part of 'Access-Control-Max-Age' header in the pre-flight response. A negative value will prevent CORS Filter from adding this response header from pre-flight response. Defaults: 1800
cors.support.credentialsA flag that indicates whether the resource supports user credentials. This flag is exposed as part of 'Access-Control-Allow-Credentials' header in a pre-flight response. It helps browser determine whether or not an actual request can be made using credentials. Defaults: true
cors.logging.enabledA flag to control logging to container logs. Defaults: false
cors.request.decorateA flag to control if the request should be decorated or not. Defaults: true

測試:#

1.服務端準備介面(我的地址是:http://192.168.10.61:8080/api)

@RequestMapping(method = RequestMethod.GET,value = "test")
@ResponseBody
public HashMap<String,Object> getArticles(){
    HashMap<String,Object> map = new HashMap<>();
    map.put("result","success");
    return map;
}

2.過濾器配置(web.xml),配置允許訪問的域為:http://192.168.56.129http://www.website2.com

<filter>
    <filter-name>CORS Filter</filter-name>
    <filter-class>org.ebaysf.web.cors.CORSFilter</filter-class>
    <init-param>
      <param-name>cors.allowed.origins</param-name>
      <param-value>http://192.168.56.129,http://www.website2.com</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>CORS Filter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

3.準備測試網頁index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>cors test page</title>
    <script src="jquery.min.js"></script>
    <script>
        function loadData(){
            $.ajax({
                url: "http://192.168.10.61:8080/api",
                type:"GET",
                dataType:"json",
                timeout:10000,
                success:function(data){
                    $("#result").append(data.result+"<br />");
                    console.log(data);
                },
                error:function(e){
                    $("#result").append(e.statusText+"<br />");
                }
            });
        }
        $(function(){
            $("#host").append("origin:"+window.location.origin);
        });
    </script>
</head>
<body>
<button onclick="loadData()">onclick</button>
<div id="host"></div>
<div id="result" style="height:200px;width:100%"></div>
</body>
</html>

4.將index.html釋出到nginx(nginx後面也有方案)

index.html 不能直接用瀏覽器開啟執行,雖然可以呼叫Ajax請求,但是域是file:///path/index.html

虛擬機器增加一個網路卡地址(原機器IP是192.168.56.129)

ifconfig eth0:0 192.168.56.130

建立兩個測試網站

cd home
mkdir /website1 #站點目錄
mkdir /website2 

將index.html 傳輸到這兩個目錄

配置nginx,增加兩個server節點

# ----server1 ----
server {
    listen       192.168.56.129:80;
    server_name  www.website1.com;
    location / {
        root   /website1;
        index  index.html index.htm;
    }
}
# ----server2 ----
server {
    listen       192.168.56.130:80;
    server_name  www.website2.com;
    location / {
        root   /website2;
        index  index.html index.htm;
    }
}

重啟nginx服務

./nginx -s reload

5.修改本地hosts檔案

//hosts檔案路徑:windows系統一般在C:\Windows\System32\drivers\etc
192.168.56.129 	www.website1.com
192.168.56.130  www.website2.com

通過增加虛擬網路卡 、nginx代理 和 修改hosts檔案,我在本地就有4個網站(域)可以進行測試了,分別是:

6.測試

準備:

(chrome)開啟4個tab,分別進入到上述四個網站,頁面列印了當前origin,通過onclick呼叫Ajax請求,頁面佈局如下

預期:

結果:




符合預期!

建議使用,除了對域的過濾,還做了其他很多操作,比簡單的自定義過濾器考慮得周全,例如

this.handlePreflightCORS(request1, response, filterChain);
this.handleNonCORS(request1, response, filterChain);
this.handleInvalidCORS(request1, response, filterChain);

總結#

cors在開發WebService、RESTful API 時經常會遇到,在以前可能直接通過jsonp解決,jsonp怎樣怎樣就不多說了。 總之,CORS技術規範出來這麼久了,如果不考慮IE6 IE7的問題,那麼還是積極擁抱CORS吧

上文三種解決方案,通過搜尋引擎均能找到,但估計大部分都是用的第一種最簡單的無腦的Cors Filter處理,第二種方案是通過nginx配置的,並不適合所有Web應用。第三種,考慮得很周全,而且使用方便,如果不考慮造重複輪子,推薦使用。

本文所用的測試工程程式碼太簡單了,就不放github了,直接下載吧,專案下載地址

斷斷續續寫了好幾天,轉載請附帶原文路徑:http://www.cnblogs.com/sloong/p/cors.html

相關文章