IE9下的跨域问题小总结

由于浏览器同源策略,凡是发送请求url的协议、域名、端口三者之间任意一与当前页面地址不同即为跨域

最近项目要兼容IE9,找了一些资料,实践了一下,现在总结一下,避免以后踩坑。

普通请求的跨域

简单粗暴的解决方案

第一次碰到这个问题,所以就是上网找找有没有什么好的解决方案。最初找到的方案是这样的,直接在IE中设置设置受信任的站点,然后允许其可以进行跨域访问,最后在jQuery中设置开启跨域请求。oh,No!这么粗暴,好吧,这也是一个不是办法的办法,如果做的是一个小项目,用户不多,那直接写在用户手册里,让他们自己去配吧。但是,这显然不是一个好的解决方案啊,那只能继续找了。

XDomainRequest(XDR)解决方案

ok,微软在IE8IE9下给我们提供了XDomainRequest来进行解决跨域问题,官方的文档可以在 这里看到。当然Github上也有开源的jQuery插件,可以在这里找到

XDR的限制:

  1. XDR仅支持GETPOST这两种请求方式,虽然可以使用上面提交的插件来解决前端部分只要进行简单修改代码就可以提交PUT/HEAD/DELETE的请求的问题,但是其请求的发生出去依旧还是将PUT/HEAD/DELETE转化为POST,将HEAD 转化为GET请求。当是POST请求的时候,请求方案会以__method=原请求的方式结构加入到请求体的body中。当是HEAD 请求的时候,请求方案会以__method=原请求的方式结构加入请求url的查询参数中。现在大部分API开发都是按照RESTful规范进行设计的,如果是自己的服务端还好,可以叫服务端的同学添加一个拦截器做一个拦截判断,然后执行对应的方法(ps:我想过去应该是这个样子,不知道服务端的同学会不会磨刀子)。但是如果你调用是网上的API的接口的话,那就爱莫能助了。

  2. XDR不支持自定义的请求头,因此如果你的服务端是用过header中的自定义参数进行做身份验证的话,那也行不通了。

  3. 请求头的Content-Type只允许设置为text/plain

  4. XDR不允许跨协议的请求,如果你的网页是在HTTP协议下,那么你只能请求HTTP协议下的接口,不能访问HTTPS 下的接口。

  5. XDR只接受HTTP/HTTPS 的请求

  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---

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值