IE9下的跨域問題小總結

因爲瀏覽器同源策略,凡是發送請求url的協議、域名、端口三者之間任意一與當前頁面地址不一樣即爲跨域javascript

最近項目要兼容IE9,找了一些資料,實踐了一下,如今總結一下,避免之後踩坑。html

普通請求的跨域

簡單粗暴的解決方案

第一次碰到這個問題,因此就是上網找找有沒有什麼好的解決方案。最初找到的方案是這樣的,直接在IE中設置設置受信任的站點,而後容許其能夠進行跨域訪問,最後在jQuery中設置開啓跨域請求。oh,No!這麼粗暴,好吧,這也是一個不是辦法的辦法,若是作的是一個小項目,用戶很少,那直接寫在用戶手冊裏,讓他們本身去配吧。可是,這顯然不是一個好的解決方案啊,那隻能繼續找了。前端

XDomainRequest(XDR)解決方案

ok,微軟在IE8IE9下給咱們提供了XDomainRequest來進行解決跨域問題,官方的文檔能夠在 這裏看到。固然Github上也有開源的jQuery插件,能夠在這裏找到java

XDR的限制:jquery

  1. XDR僅支持GETPOST這兩種請求方式,雖然可使用上面提交的插件來解決前端部分只要進行簡單修改代碼就能夠提交PUT/HEAD/DELETE的請求的問題,可是其請求的發生出去依舊仍是將PUT/HEAD/DELETE轉化爲POST,將HEAD 轉化爲GET請求。當是POST請求的時候,請求方案會以__method=原請求的方式結構加入到請求體的body中。當是HEAD 請求的時候,請求方案會以__method=原請求的方式結構加入請求url的查詢參數中。如今大部分API開發都是按照RESTful規範進行設計的,若是是本身的服務端還好,能夠叫服務端的同窗添加一個攔截器作一個攔截判斷,而後執行對應的方法(ps:我想過去應該是這個樣子,不知道服務端的同窗會不會磨刀子)。可是若是你調用是網上的API的接口的話,那就心有餘而力不足了。git

  2. XDR不支持自定義的請求頭,所以若是你的服務端是用過header中的自定義參數進行作身份驗證的話,那也行不通了。github

  3. 請求頭的Content-Type只容許設置爲text/plainweb

  4. XDR不容許跨協議的請求,若是你的網頁是在HTTP協議下,那麼你只能請求HTTP協議下的接口,不能訪問HTTPS 下的接口。ajax

  5. XDR只接受HTTP/HTTPS 的請求apache

  6. 發起請求的時候,不會攜帶authentication cookies

JSONP

JSONP的本質是動態的加載<script> 標籤,所以其只支持GET請求而不支持其餘類型的HTTP請求。

JSONP 的執行過程大體以下:

  1. 客戶端設置一個全局的function,而後使用callback=function 的方法,將回調的方法傳遞給服務端。例如:

    // 定義全局函數
    function showData (data) {
      console.log(data)
    }
    var url = "http://test.com/jsonp/query?id=1&callback=showData" // 這個就是script標籤中的url
  2. 服務端在接收到請求的時候,生成一個動態的js腳本,在該腳本中,調用callback參數傳遞進來的function,將回來返回的json 數據已參數的形式去傳遞給該function,這樣,客戶端在加載這個js的時候,就會自動去執行了。

代理

其實,跨域的根本問題就在於,你調用的服務端地址web地址不在同一個域下,那麼,咱們最容易想到的一個解決方案就是:那我把他們放在一個域下面不就能夠了麼。所以咱們能夠在web工程下 放置一個代理服務器,在IE10如下的瀏覽器中,咱們的網絡請求統一走這一個代理接口,由服務器帶咱們去轉發這個HTTP請求,而後再將結果返回給咱們。

事實上咱們項目中也是採用的這個方案,咱們定義了一個接口:

  • URL: v0.1/dispatcher

  • 方法: POST

  • 請求內容:

{
  "request_url":"http://test.com", //必填,請求url
  "request_method":"POST", //必填,請求方法:GET/PUT/PATCH/POST/DELETE
  "request_headers":{
    "Content-Type":["application/json"]
  }, //選填,請求頭
  "request_data":{
    "data":{    
          //請求body
    }
  }
} //選填,請求body

服務端經過客戶端傳來的這些參數去構造一個HttpClient ,發起請求。

文件上傳的問題

既然經過上面的代理接口解決了,IE10 一下的跨域請求問題,本想着應該沒什麼問題了,試了試項目中的文件上傳,oh,no!不能運行,看了看咱們的文件上傳,是經過本身new FormData()的方式去向服務器POST請求的。而後翻找了一下webApi, 發現從IE10 開始兼容的,這就......,而且XMLHttpRequestsend(formData)這個方法也是從IE10開始支持的。那沒辦法了只能尋找其餘的辦法了。

隱式表單上傳

找到老司機,請教了一下,早期IE都是用使用隱式的iframe中包含一個form表單,而後直接去提交form表單。而後服務徹底返回的數據在iframe中,經過js代碼去裏面獲取iframe中的數據,做爲返回值。

而後從老司機那邊獲得一份插件ajaxfileupload,還有一個就是本身在Github上找的一個jQuery-File-Upload,如今就來說講這兩個插件

ajaxfileupload

適用於服務器返回的數據是文本格式

這份代碼也很簡單就200多行,主要就思想就是根據上面說的,使用隱式的iframe嵌套form表單來完成上傳操做。可是呢?這個插件只適合在服務器返回數據是文本數據的時候,若是服務器返回的是json 的數據,IE10一下的瀏覽器就會自動去執行下載操做,js代碼在執行到下載的時候就中斷了,並不會繼續往下執行了。因此也不是很適用。若是服務器支持返回數據格式是文本格式的話,這個組件仍是挺好用的。

// 基本用法以下
<!-- 隱藏file標籤 -->  
<input id="fileUpload" style="display: none" type="file" name="file">
 //選擇文件以後執行上傳  
    $('#fileUpload').on('change', function() {  
        $.ajaxFileUpload({  
            url:'http://test.com',  
            secureuri:false,  
            fileElementId:'fileToUpload',//file標籤的id  
            dataType: 'json',//返回數據的類型  
            data:{name:'logan'},//一同上傳的數據  
            success: function (data, status) {  
               console.log(data)
            },  
            error: function (data, status, e) {  
                alert(e);  
            }  
        });  
    });

jQuery-File-Upload

適用於服務器返回的數據是JSON格式切支持重定向

這個插件呢,對比ajaxfileupload他考慮到了這種返回json的狀況,可是它的使用須要服務端進行支持,其主要思想仍是使用了隱式的表單上傳文件,可是它是經過服務其的重定向來接收數據的,服務器接收到了客戶端的請求以後,將返回的數據經過URLEncode以後,拼接在前端web頁面的後面,而後在頁面中解析數據,寫到body中,用jQuery去獲取這些數據。

具體用法以下:

如今服務器構造一個接受返回數據的頁面result.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>result</title>
</head>
<body>
<script>
  var href = window.location.href
  var search = href.slice(href.indexOf('?') + 1)
  document.body.innerText=document.body.textContent=decodeURIComponent(search)
</script>
</body>
</html>

而後本身定義一個上傳的組件,我這裏是使用Vue來包裝成一個組件的

<template>
  <div class="c-uploader"
       v-tap
       @click="onTap">
    <input
      type="file"
      ref="file"
      id="fileUploadNormal"
      name="file"
      style="display: none"
      data-sequential-uploads="true"
      :accept="accept"
      multiple="false"/>
    <slot></slot>
  </div>
</template>
<script>
  import { getMACContent } from '../../utils/tokens'
  import optionsUtil from '../../utils/optionsUtil'
  import tap from '../directives/tap'
  import '../libs/vendor/jquery.ui.widget'
  import '../libs/jquery.iframe-transport'
  import '../libs/jquery.fileupload'
  import '../libs/cors/jquery.xdr-transport'
  import g_config from '../../config/config'
  export default{
    props: {
      url: {
        type: String,
        required: true
      },
      data: {
        type: Object,
        default: function () {
          return {}
        }
      },
      accept: {
        type: String,
        default: '*'
      },
      onSuccess: {
        type: Function,
        default: function () {
        }
      },
      onError: {
        type: Function,
        default: function () {
        }
      },
      checkFile: {
        type: Function,
        default: function () {
          return true
        }
      }
    },
    data () {
      return {
        uploadFile: null,
        headers: {
          Authorization: new getMACContent({url: this.url,method: 'POST'})._value.returnMessage
        }
      }
    },
    methods: {
      onTap (e) {
        const fileInputEl = this.$refs.file
        if (!fileInputEl) {
          return
        }
        // TODO: trigger tap or touch but not click ?
        fileInputEl.click()
        fileInputEl.value = ''
      },
      init () {
        this.uploadFile = null
      },
      startUpload () {
        const that = this
        if (this.uploadFile !== null) {
          if (this.checkFile(this.uploadFile.files[0])) {
            that.data.request_url = that.url
            that.data.name = this.uploadFile.files[0].name
            that.data.redirect = location.protocol+'//' + location.host+'/result.html?'
            that.data.authorization = that.headers.Authorization
            $('#fileUploadNormal').fileupload({
              url: g_config.dispatch_url + '/v0.1/dispatcher/upload',
              formData: that.data
            })
            that.uploadFile.submit() // 上傳文件
          }
        } else {
          const response = {
            msg: '請選擇須要上傳的文件'
          }
          this.onError(null, response, null)
        }
      }
    },
    computed: {},
    watch: {},
    components: {
    },
    directives: {
      tap
    },
    mounted () {
      const that = this
      $('#fileUploadNormal').fileupload({
        dataType: 'json', // 設置返回數據格式
        multiple: false, // 只容許選擇單文件
        iframe: true, // 使用iframe
        sequentialUploads: true,
        forceIframeTransport : true, // 強制使用iframe
        autoUpload: false, // 關閉自動上傳,不然在文件變化的時候,就會自動upload
        formData: that.data, // 定義須要格外上傳的數據
        replaceFileInput: false,
        add: function (e, data) {
          that.headers.Authorization = new getMACContent({url: that.url,method: 'POST'})._value.returnMessage
          that.uploadFile = data // 記錄下數據
        },
        done: function (e, data) {
          const response = data.result
          const resultData = JSON.parse(response.data)
          if (response.result.toUpperCase() === 'SUCCESS') {
            that.onSuccess(resultData)
          } else {
            that.onError(null,resultData, null)
          }
        },
        fail: function (e, data) {
          that.uploadFile = null
          that.onError(null,{
            msg: '上傳失敗'
          }, null)
        }
      })
    }
  }
</script>

這個插件是依賴jQuery的,而且依賴jQuery-UI ,還有要注意的是在IE10如下的版本都要引入jquery.iframe-transport

jquery.xdr-transport

我代碼中發送數據的方式是它在add 方法中返回的data數據,經過該對象去直接上傳文件,這時上傳的FormData的文件信息中,文件本來是什麼類型就是什麼類型了,這是咱們所指望的。我以前查看官方的文檔,還使用過另外一種方式

var jqXHR = $('#fileupload').fileupload('send', {files: filesList})
    .success(function (result, textStatus, jqXHR) {/* ... */})
    .error(function (jqXHR, textStatus, errorThrown) {/* ... */})
    .complete(function (result, textStatus, jqXHR) {/* ... */});

上傳的時候使用的是這樣的方式,發現FormData中上傳文件的類型變爲了Content-Type: application/octet-stream,而後服務器就解析不到數據了。因此仍是推薦用它原生的submit方式去提交數據。

注意

這兩個插件的本質仍是使用form表單上傳文件,所以咱們沒法添加自定義的header頭,而且若是原來的服務器不支持請求重定向的話怎麼辦,那就沒有辦法使用jQuery-File-Upload這個插件了。因此最穩妥的方式,仍是在咱們本地作了一層代理,由代理去發生真正的請求。

下面給出主要的轉發FormDatajava代碼

public ResponseEntity dispatcherUpload(HttpServletRequest request) throws UnsupportedEncodingException {

    String requestUrl = request.getParameter("request_url");
    String redirectUrl = request.getParameter("redirect");
    String fileName = request.getParameter("name");

    if (StringUtils.isEmpty(requestUrl) || StringUtils.isEmpty(redirectUrl))
      throw new BizException(ErrorCode.INVALID_ARGUMENT);

    HttpClient httpClient = new DefaultHttpClient();
    HttpPost httpPost = new HttpPost(requestUrl);
    String auth = request.getParameter("authorization");
    if (!StringUtils.isEmpty(auth))
      httpPost.addHeader("Authorization", request.getParameter("authorization").toString());
    MultipartEntity reqEntity = new MultipartEntity();

    if (!StringUtils.isEmpty(request.getParameter("path"))) {
      StringBody pathBody = new StringBody(request.getParameter("path"));
      reqEntity.addPart("path", pathBody);
    }
    if (!StringUtils.isEmpty(request.getParameter("scope"))) {
      StringBody scopeBody = new StringBody(request.getParameter("scope"));
      reqEntity.addPart("scope", scopeBody);
    }
    if (!StringUtils.isEmpty(request.getParameter("expireDays"))) {
      StringBody expireDaysBody = new StringBody(request.getParameter("expireDays"));
      reqEntity.addPart("expireDays", expireDaysBody);
    }
    if (!StringUtils.isEmpty(fileName)) {
      StringBody nameBody = new StringBody(fileName);
      reqEntity.addPart("name", nameBody);
    }

    MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest) request;
    MultiValueMap<String, MultipartFile> multiValueMap = multipartHttpServletRequest.getMultiFileMap();
    //todo:如今暫時寫死,不去遍歷map
    if(!(multiValueMap.containsKey(CS_FILE_KEY) || multiValueMap.containsKey(UC_FILE_KEY)))
      throw new BizException(ErrorCode.INVALID_ARGUMENT);
    String fileKey = multiValueMap.containsKey(CS_FILE_KEY) ? CS_FILE_KEY : UC_FILE_KEY;
    MultipartFile multipartFile = multipartHttpServletRequest.getFile(fileKey); // 獲得文件數據
    if (!multipartFile.isEmpty()) {

      CommonsMultipartFile commonsMultipartFile = (CommonsMultipartFile) multipartFile;
      DiskFileItem diskFileItem = (DiskFileItem) commonsMultipartFile.getFileItem();
      String filePath = diskFileItem.getStoreLocation().getPath().toString();

      File file = null;
      try {
        //判斷目錄是否已存在,若是filename不爲空,將其帶入建立文件(真實還原文件類型,不然是.tmp臨時文件)
        if (StringUtils.isEmpty(fileName)) {
          file = new File(filePath);
        } else {
          file = new File(filePath, fileName);
        }
        if (!file.exists()) {
          file.mkdirs();
        }
        //保存文件
        multipartFile.transferTo(file);
        FileBody bin = new FileBody(file);
        reqEntity.addPart(fileKey, bin);
        httpPost.setEntity(reqEntity);

        HttpHeaders responseHeader = new HttpHeaders();
        HttpResponse httpResponse = null;
        try {
          httpResponse = httpClient.execute(httpPost);
        } catch (Exception e) {
          LOG.error("代理文件上傳失敗,請求地址:{},請求內容:{}", requestUrl, null, e);
          JSONObject failedJson = new JSONObject();
          failedJson.put("result", "FAILURE");
          failedJson.put("data", e.toString());
          URI uri = URI.create(redirectUrl + e.toString());
          responseHeader.setLocation(uri);
          return new ResponseEntity(responseHeader, HttpStatus.MOVED_TEMPORARILY);
        }
        LOG.info("狀態碼:" + httpResponse.getStatusLine().getStatusCode());
        org.apache.http.HttpEntity httpEntity = httpResponse.getEntity();
        //判斷請求是否成功
        String responseBody = "";
        String isSuccess = "SUCCESS";
        if (httpResponse.getStatusLine().getStatusCode() >= HttpStatus.OK.value() && httpResponse.getStatusLine().getStatusCode() < HttpStatus.BAD_REQUEST.value()) {
          if (null != httpEntity) {
//            System.out.println("響應內容:" + EntityUtils.toString(httpEntity, ContentType.getOrDefault(httpEntity).getCharset()));
            responseBody = EntityUtils.toString(httpEntity, ContentType.getOrDefault(httpEntity).getCharset());
            //處於安全考慮,關閉數據流
            EntityUtils.consume(httpEntity);
          }
        } else {
          //上傳失敗(非2XX)
          isSuccess = "FAILURE";
        }
        JSONObject ResJson = new JSONObject();
        ResJson.put("result", isSuccess);
        ResJson.put("data", responseBody);
        URI uri = URI.create(redirectUrl + URLEncoder.encode(ResJson.toString(), "UTF-8"));
        responseHeader.setLocation(uri);
        return new ResponseEntity(responseHeader, HttpStatus.MOVED_TEMPORARILY);
      } catch (IOException e) {
        throw new BizException(ErrorCode.INTERNAL_SERVER_ERROR, e);
      } finally {
        if (file != null) {
          file.delete();
        }
      }
    }else {
      throw new BizException(HttpStatus.BAD_REQUEST, "PORTAL-APP/INVALID_ARGUMENT", "上傳文件爲空");
    }
  }

在轉發文件的時候,咱們作了一層轉存,緣由在於,咱們測試一個服務器的時候,咱們直接使用一個緩存的數據,去寫到FormData中,那邊服務器接收到的文件對象竟然是空的,所以咱們才作了一層緩存,用一個真實存在的文件去作。

---end---

相關文章
相關標籤/搜索