JavaScript 跨域訪問的問題和解決過程

受瀏覽器的同源策略限制,JavaSript只能請求本域內的資源。跨域資源共享(Cross-Origin Resource Sharing, CORS)是爲解決Ajax技術難實現跨域問題而提出的一個規範,這個規範試着從根本上解決安全的跨域資源共享問題。在此之前,解決此類問題的途徑往往是服務器代理、JSONP等,治標不治本。目前基本所有瀏覽器都已經支持該規範。

一個域是由schema、host、port三者共同組成,與路徑無關。所謂跨域,是指在http://example-foo.com/域上通過XMLHttpRequest對象調用http://example-bar.com/域上的資源。CORS約定服務器端和瀏覽器在HTTP協議之上,通過一些額外HTTP頭部信息,進行跨域資源共享的協商。服務器端和瀏覽器都必需遵循規範中的要求。

CORS把HTTP請求分成兩類,不同類別按不同的策略進行跨域資源共享協商。

1. 簡單跨域請求。
當HTTP請求出現以下兩種情況時,瀏覽器認爲是簡單跨域請求:

1). 請求方法是GET、HEAD或者POST,並且當請求方法是POST時,Content-Type必須是application/x-www-form-urlencoded, multipart/form-data或着text/plain中的一個值。
2). 請求中沒有自定義HTTP頭部。

對於簡單跨域請求,瀏覽器要做的就是在HTTP請求中添加Origin Header,將JavaScript腳本所在域填充進去,向其他域的服務器請求資源。服務器端收到一個簡單跨域請求後,根據資源權限配置,在響應頭中添加Access-Control-Allow-Origin Header。瀏覽器收到響應後,查看Access-Control-Allow-Origin Header,如果當前域已經得到授權,則將結果返回給JavaScript。否則瀏覽器忽略此次響應。

2. 帶預檢(Preflighted)的跨域請求。
當HTTP請求出現以下兩種情況時,瀏覽器認爲是帶預檢(Preflighted)的跨域請求:

1). 除GET、HEAD和POST(only with application/x-www-form-urlencoded, multipart/form-data, text/plain Content-Type)以外的其他HTTP方法。
2). 請求中出現自定義HTTP頭部。

帶預檢(Preflighted)的跨域請求需要瀏覽器在發送真實HTTP請求之前先發送一個OPTIONS的預檢請求,檢測服務器端是否支持真實請求進行跨域資源訪問,真實請求的信息在OPTIONS請求中通過Access-Control-Request-Method Header和Access-Control-Request-Headers Header描述,此外與簡單跨域請求一樣,瀏覽器也會添加Origin Header。服務器端接到預檢請求後,根據資源權限配置,在響應頭中放入Access-Control-Allow-Origin Header、Access-Control-Allow-Methods和Access-Control-Allow-Headers Header,分別表示允許跨域資源請求的域、請求方法和請求頭。此外,服務器端還可以加入Access-Control-Max-Age Header,允許瀏覽器在指定時間內,無需再發送預檢請求進行協商,直接用本次協商結果即可。瀏覽器根據OPTIONS請求返回的結果來決定是否繼續發送真實的請求進行跨域資源訪問。這個過程對真實請求的調用者來說是透明的。

XMLHttpRequest支持通過withCredentials屬性實現在跨域請求攜帶身份信息(Credential,例如Cookie或者HTTP認證信息)。瀏覽器將攜帶Cookie Header的請求發送到服務器端後,如果服務器沒有響應Access-Control-Allow-Credentials Header,那麼瀏覽器會忽略掉這次響應。

這裏討論的HTTP請求是指由Ajax XMLHttpRequest對象發起的,所有的CORS HTTP請求頭都可由瀏覽器填充,無需在XMLHttpRequest對象中設置。以下是CORS協議規定的HTTP頭,用來進行瀏覽器發起跨域資源請求時進行協商:
1. Origin。HTTP請求頭,任何涉及CORS的請求都必需攜帶。
2. Access-Control-Request-Method。HTTP請求頭,在帶預檢(Preflighted)的跨域請求中用來表示真實請求的方法。
3. Access-Control-Request-Headers。HTTP請求頭,在帶預檢(Preflighted)的跨域請求中用來表示真實請求的自定義Header列表。
4. Access-Control-Allow-Origin。HTTP響應頭,指定服務器端允許進行跨域資源訪問的來源域。可以用通配符*表示允許任何域的JavaScript訪問資源,但是在響應一個攜帶身份信息(Credential)的HTTP請求時,Access-Control-Allow-Origin必需指定具體的域,不能用通配符。
5. Access-Control-Allow-Methods。HTTP響應頭,指定服務器允許進行跨域資源訪問的請求方法列表,一般用在響應預檢請求上。
6. Access-Control-Allow-Headers。HTTP響應頭,指定服務器允許進行跨域資源訪問的請求頭列表,一般用在響應預檢請求上。
7. Access-Control-Max-Age。HTTP響應頭,用在響應預檢請求上,表示本次預檢響應的有效時間。在此時間內,瀏覽器都可以根據此次協商結果決定是否有必要直接發送真實請求,而無需再次發送預檢請求。

8. Access-Control-Allow-Credentials。HTTP響應頭,凡是瀏覽器請求中攜帶了身份信息,而響應頭中沒有返回Access-Control-Allow-Credentials: true的,瀏覽器都會忽略此次響應。

總結:只要是帶自定義header的跨域請求,在發送真實請求前都會先發送OPTIONS請求,瀏覽器根據OPTIONS請求返回的結果來決定是否繼續發送真實的請求進行跨域資源訪問。所以複雜請求肯定會兩次請求服務端。


二、代碼操作


解決跨域調用服務並設置headers 主要的解決方法需要通過服務器端設置響應頭、正確響應options請求,正確設置 JavaScript端需要設置的headers信息 方能實現。

1.第一步 服務端設置響應頭,在webapi的web.config做如下設置

 <system.webServer>
     <httpProtocol>
      <!--跨域配置開始-->
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*" /><!--支持全域名訪問,不安全,部署後需要固定限制爲客戶端網址-->
        <add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS" /><!--支持的http 動作-->
        <add name="Access-Control-Allow-Headers" value="Content-Type,X-Requested-With,token" /><!--響應頭 請按照自己需求添加 這裏新加了token這個headers-->
        <add name="Access-Control-Request-Methods" value="GET, POST, PUT, DELETE, OPTIONS" /><!--允許請求的http 動作-->
      </customHeaders>
      <!--跨域配置結束-->
    </httpProtocol>

2.第二部 瞭解IE chrome 等瀏覽器 對於 跨域請求並要求設置Headers自定義參數的時候的 "預請求"   就是如果遇到 跨域並設置headers的請求,所有請求需要兩步完成!

A 第一步:發送預請求 OPTIONS 請求。此時 服務器端需要對於OPTIONS請求作出響應 一般使用202響應即可 不用返回任何內容信息。(能看到這份手稿的人,本人不相信你後臺處理不了一個options請求)options請求可在權限攔截器中處理

  

複製代碼
複製代碼
  /// <summary>
    /// 權限攔截器
    /// </summary>
    public class ApiAuthorizeAttribute : AuthorizeAttribute
    {
       public override void OnAuthorization(HttpActionContext actionContext)
        {
            if (actionContext.Request.Method == HttpMethod.Options)
            {
                actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Accepted);
                return;
            }
       }
    }
複製代碼
複製代碼

 

B 第二步:服務器accepted 第一步請求後 瀏覽器自動執行第二步 發送真正的請求。

客戶端代碼:

 

複製代碼
複製代碼
  $("#btnSumit").click(function () {
                var Ticket = $.cookie("token");
                var model = {
                    id: 1
                };
                $.ajax({
                    type: "POST",
                    url: "http://localhost:65312/api/products/FindProductById",
                    data: JSON.stringify(model),
                    contentType: "application/json; charset=utf-8",
                    dataType: "json",
                    beforeSend: function (xhr) {
                    //    //發送ajax請求之前向http的head裏面加入驗證信息
                        xhr.setRequestHeader("token", Ticket);  // 請求發起前在頭部附加token
                    },
                    success: function (data, status) {
                        if (data.statuscode == "401") {
                            alert(data.msg);
                        }
                        else
                        {
                            alert(JSON.stringify(data))
                        }
                    },
                    //error: function (XMLHttpRequest, textStatus, errorThrown) {
                    //    alert(XMLHttpRequest.status);
                    //    alert(XMLHttpRequest.readyState);
                    //    alert(textStatus);
                    //},
                    complete: function () {

                    }

                });
            });
複製代碼
複製代碼


三、可能遇到的問題


1.原本的代碼很簡單。。如果是同域名什麼問題都沒有 (有興趣的朋友可以嘗試在自己的服務器上運行以下代碼)

複製代碼
           $.ajax({
                url: "http://www.google.com/", //不同域名,而且google 沒有允許第三方提交所以會出錯
                cache: false,
                //data: params,
                dataType: 'json',
                success: function (data) {
                    console.log(data);
                },
                error: function (e) {
                    alert(e.statusText);
                }
            })
複製代碼

嗯,我的默認瀏覽器是Chrome, 上去一跑。。。當然不能用。。。什麼都還沒做呢,就想做跨域訪問這麼危險的事情

下面是Chrome給出的錯誤提示 

  

2.在服務器端做點手腳,

                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", "*"); // 可以設置爲詳細的地址


3. 好了現在Chrome中的Get已經可以運行了,依葫蘆畫瓢開發了Post方法。。。。發現Post不能用。。。。- -# 真是不順利啊

在Fiddler中發現客戶端提交的是OPTIONS的請求。。。。恩。。。。。那就加一段邏輯處理OPTIONS

複製代碼
                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", "*"); // 可以設置爲詳細的地址

                if (HttpContext.Current.Request.HttpMethod == "OPTIONS") // 加點邏輯
                {
                    HttpContext.Current.Response.AddHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
                    HttpContext.Current.Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept,X-Requested-With");
                    HttpContext.Current.Response.End();
                }
 
複製代碼

實際運行中有兩次請求 第一次是OPTIONS 第二次纔是POST

 

4.還有問題。。。。忽然發現在IE8和IE9中無法運行,而在其他的瀏覽器中都正常(opera未測試,google說這個瀏覽器也有問題。不過這東西比較小衆)

使用Fiddler發現 這個動作根本沒有被提交到服務器端。。。。

經常Google以後發現。。。。IE8以上的版本跨域提交需要使用XDomainRequest 對象。。。。(IE 爲什麼每次你都這麼另類!,jQuery你爲什麼不兼容ie8和ie9的跨域提交功能。。加點代碼很麻煩麼!!!)

複製代碼
 var xdr = new XDomainRequest();
        xdr.onload = function (e) {
            var data = $.parseJSON(xdr.responseText);
            if (data == null || typeof (data) == 'undefined') {
                data = $.parseJSON(data.firstChild.textContent);
            }
            //success
        };
        xdr.onerror = function (e) {
            //error
        }

        xdr.open("GET", url);
        xdr.send();
複製代碼

關於 XDomainRequest 請在這裏查看詳細,http://msdn.microsoft.com/en-us/library/cc288060(VS.85).aspx

 

5.恩 get功能在ie中也可以了。。。不錯不錯, POST還不行。。。莫非又是IE的問題?? 這。。怎麼每個功能都這麼多問題?

奇怪的是Fiddler中顯示IE8 中POST請求確實發出去了啊。。。怎麼回事??

把問題分解來看,吧fiddler獲取的http request raw數據拿出來 單獨提交試試。。。也不行?! 服務器返回415。。。 看來好像不是ie的問題。(這次冤枉了它了)

仔細排查,發現缺少Content-Type(Content-Type其實不是必須的,參考RFC)

這坑爹的WCF 3.5啊, 不傳Content Type就給我報415異常  (WCF 4.0已經解決這個問題,3.5解決起來很麻煩,我一怒之下用了普通的ashx來處理)

.....嗯。。。少什麼我加什麼。。。 what?!!! XDomainRequest 不能隨便設置header,    

var xdr=new XDomainRequest ();
xdr.contentType="application/json"; //異常。。。。。。。
xdr.contentType = "text/plain";  //這是唯一可以設置的值。。。。MS。。。我要json不要這個。。。。

好吧javascript這邊設置失敗了。。只能去服務器動手腳。。。想死的心都有了。。。。做點功能怎麼這麼麻煩。。。

到此爲止,總算告一個段落了。。。

 

附錄1

用來解決跨域問題的,服務器端代碼

複製代碼
    public class Global : System.Web.HttpApplication
    {
        protected void Application_BeginRequest(object sender, EventArgs e)
        {
            if (HttpContext.Current != null && HttpContext.Current.Response != null)
            {
                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", "*"); // take care

                if (HttpContext.Current.Request.HttpMethod == "OPTIONS")
                {
                    HttpContext.Current.Response.AddHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
                    HttpContext.Current.Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept,X-Requested-With");
                    HttpContext.Current.Response.End();
                }
            }
        }

    }
複製代碼

服務器也可以通過放置crossdomain.xml在根目錄下指定該邏輯,參考http://www.weibo.com/crossdomain.xml 

 

附錄2 用來解決客戶端問題的參考代碼(代碼比較潦草,不過重要的是邏輯)

複製代碼
function cloverGet(url, params, isRenderLoading, callback) {
    if ($.browser.msie && parseInt($.browser.version, 10) >= 8 && window.XDomainRequest) {
        var xdr = new XDomainRequest();

        xdr.onload = function (e) {
            var data = $.parseJSON(xdr.responseText);
            if (data == null || typeof (data) == 'undefined') {
                data = $.parseJSON(data.firstChild.textContent);
            }
            //需要手動處理json數據
        };
        xdr.onerror = function (e) {
          
        }
        xdr.open("GET", url);
        xdr.send();
    }
    else {
        $.ajax({
            url: url,
            cache: false,
            data: params,
            dataType: 'json',
            success: function (data) {
               
            },
            error: function (e) {
                
            },
            complete: function (e) {
               
            },
            beforeSend: function (xhr) {
              
            }
        });



    }
}
function cloverPost(url, params, callback) {

    if ($.browser.msie && parseInt($.browser.version, 10) >= 8 && window.XDomainRequest) {
        var xdr = new XDomainRequest();
        xdr.contentType = "text/plain";
        xdr.onload = function () {
            var data = $.parseJSON(xdr.responseText);
            if (data == null || typeof (data) == 'undefined') {
                data = $.parseJSON(data.firstChild.textContent);
            }
            //需要手動格式化
        };
        xdr.onerror = function (e) {

        }
        xdr.open("POST", url);
        xdr.send(params); //這裏的數據是 a=1&b=2這樣的

    }
    else {
        $.ajax({
            type: "POST",
            url: url,
            data: params,
            // dataType: "json",  //有的時候jsonp也是一個選擇
            crossDomain: true,
            success: function (data) {

            },
            error: function (e) {
                 
            },
            complete: function (e) {
                 
            },
            beforeSend: function (xhr) {
                 
            }
        });
    }
}
複製代碼

 

 

  • get和post都行了。。。還有文件上傳呢。。。這樣的POST是不能傳文件的。。 (考慮用第三方方案或者不要直接提交)
  • 善用工具 例如Fiddler 還有javascript/HTML調試工具 (我個人覺得Chrome和FF的調試器比較好用)似乎不少人還習慣用alert調試。。。。
  •  IE和safari 跨域iframe有問題, 記得設置header,例如: Response.Headers["p3p"] = "CP=IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT";
  • 有興趣的朋友可以瞭解一下XSS和CSRF,這可是網站的一大安全問題
  • 分解問題是排查問題的一個很好的辦法
  • 更多的時候,使用同域名的代理服務器是很好的解決方案 (也是唯一的解決方案,如果瀏覽器直接調用第三方有權限問題的話)

發佈了6 篇原創文章 · 獲贊 17 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章