XMLHttpRequest 跨域时产生了 OPTIONS 请求

 

一:前言

对于跨域请求,一直没有采用jsonp方式,原因如下

1.jsonp只支持get请求而不支持post请求,如果想传给后台一个json格式的数据,浏览器会返回一个415的状态码,告诉我们请求格式不正确,这让传输大规模数据变得繁琐。

2.无法准确定位和调试请求异常情况

3.存在安全性问题(可能是我的技术盲点,因为看到很多大公司都用jsonp技术)

考虑到以上问题,并且跨域资源共享标准 允许XMLHttpRequest 或 Fetch 发起跨域 HTTP 请求,前后端约定数据请求一律采用XMLHttpRequest,通过后台设置响应报文头 Header set Access-Control-Allow-Origin *,即可实现跨域访问。为了防止XSS攻击, 我们又进行域名限制,比如 Access-Control-Allow-Origin: http://www.xudihui.com

二:正文

用了好几个项目下来,一直没出问题。今天维护老项目时,发现请求新接口并不能准确拿到业务数据,而是触发了一个OPTIONS请求,请求头如下:

1

2

3

4

5

6

7

8

9

10

11

OPTIONS http://activity.96225.com/win_smk_activity/baseUser/getUserIdByToken.ext HTTP/1.1

Host: activity.96225.com

Connection: keep-alive

Access-Control-Request-Method: POST

Origin: http://192.168.2.176:4000

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.98 Safari/537.36

Access-Control-Request-Headers: appid

Accept: */*

Referer: http://192.168.2.176:4000/

Accept-Encoding: gzip, deflate, sdch

Accept-Language: zh-CN,zh;q=0.8

 

从请求头来看,OPTIONS请求前端代码并没有发起,仔细查看请求头字段:

字段 Access-Control-Request-Method 告知服务器,实际请求将使用 POST 方法;

字段 Access-Control-Request-Headers 告知服务器,实际请求将携带一个自定义请求首部字段:appid,appid是用来告知服务端业务逻辑使用,ajax被封装之后,appid携带在该项目所有请求头中;

字段 Host 告诉我们服务器主机名;

字段 Referer 显示了本地开发地址,Host和Referer是典型的跨域请求。 

带着疑问去了解OPTIONS请求,首先查看了ajax方法,除了xhr对象序列和增加了一个请求头appId之外,并没有其它逻辑,代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

var ajax_ = function(obj) {

        obj = obj || {};

        obj.type = (obj.type || 'GET').toUpperCase();

        obj.dataType = obj.dataType || 'json';

        obj.timeout = obj.timeout || 20000;

        var params = formatParams(obj.data); //参数格式化

        var xhrTimeout;//前端定时放弃

        //step1:兼容性创建对象

        if (window.XMLHttpRequest) {

            var xhr = new XMLHttpRequest();

        else {

            var xhr = new ActiveXObject('Microsoft.XMLHTTP');

        }

        if(AJAX_ == 0){

          UI.showIndicator();

        }

        AJAX_++;

        //step4: 接收

        xhr.onreadystatechange = function() {

            if (xhr.readyState == 4) {

                 AJAX_--;

                if(AJAX_== 0){

                  UI.hideIndicator();

                }

                try{

                    if (xhr.status >= 200 && xhr.status < 300) {

                        obj.success && obj.success(xhr.responseText, xhr.responseXML);

                    else {

                        obj.error && obj.error(xhr.status);

                    }                     

                }catch(e){} 

            }

        }

        //step2 step3:连接 和 发送

        if (obj.type == 'GET') {

            xhr.open('GET', obj.url + '?' + params, true);

            xhr.setRequestHeader('appId', $APPID);

            xhr.send(null);

        else if (obj.type == 'POST') {

            xhr.open('POST', obj.url, true);

            //设置请求头,以表单形式提交数据

            xhr.setRequestHeader('Content-Type''application/x-www-form-urlencoded');

            xhr.setRequestHeader('appId', $APPID);

            xhr.send(params);

        }

         // 超时,默认20秒,直接设置timeout属性:https://www.w3.org/TR/2012/WD-XMLHttpRequest-20120117/#handler-xhr-ontimeout

         xhr.timeout = obj.timeout;

         xhr.ontimeout = function(){

            UI.toast('请检查网络连接是否正常',2000,'exception');

         }

 

        //辅助函数,格式化参数

        function formatParams(data) {

            var arr = [];

            for (var name in data) {

                arr.push(encodeURIComponent(name) + "=" + encodeURIComponent(data[name]));

            }

            //设置随机数,防止缓存

            arr.push("t=" + Math.random());

            return arr.join("&");

        }

    }

通过stackoverflow的这篇文章,得知我遇到的OPTIONS是浏览器发起的'preflight'请求,征求服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。但是我其它项目中并没有产生OPTIONS请求,它是从哪里来的呢,继续查看触发条件,当满足以下条件时,浏览器主动触发OPTIONS请求:

1、使用了下面任一 HTTP 方法:

  – PUT

  – DELETE

  – CONNECT

  – OPTIONS

  – TRACE

  – PATCH

2、人为设置了对 CORS 安全的首部字段集合之外的其他首部字段。该集合为:

  – Accept

  – Accept-Language

  – Content-Language

  – Content-Type (but note the additional requirements below)

  – DPR

  – Downlink

  – Save-Data

  – Viewport-Width

  – Width

3、 Content-Type 的值不属于下列之一:

  – application/x-www-form-urlencoded

  – multipart/form-data

  – text/plain

条件1并没有匹配,前端采用标准的POST请求;条件三也没有匹配,请求头Content-Type也是标准值:xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');

主要原因就是条件2,我们定义了非安全的首部字段appId,设值的初衷,就是让服务端通过此它能更方便地来做业务逻辑。

在前端把xhr.setRequestHeader('appId', $APPID)去掉之后,浏览器不进行OPTIONS服务器预检测,直接进入业务逻辑,正确发送POST请求,并且跨域成功。

到此处,其实仅仅解决我们的跨域问题已经完成了,只要把appId放到请求send数据中去,移除头部appId字段,就能成功进行前后端交互了。但appId这个字段当初放在头部,就是考虑到它有别于业务逻辑,跟业务代码一起放在请求体中不是特别合适,决定再看看有没有其它方法,允许设置自定义头并且成功跨域的方案。

感谢mozilla developer,提供了关于跨域非常详细的资料,大家可以看看。里面也提到了我当前场景的解决方案,马上着手开始实践,为了创造一个跨域环境,我先用nginx起一个127.0.0.1:8080的服务器,然后在这个地址中使用XMLHttpRequest 对象,发起目标为192.168.1.6地址的请求,192.168.1.6使用node js创建一个带有基础响应头的服务器,代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

const http = require('http');

const hostname = '192.168.1.6';  //使用ipconfig -all查看本机IP地址,然后使用127.0.0.1地址进行访问

const port = 3000;

 

//支持跨域的服务器搭建

const server = http.createServer((req, res) => {

  res.statusCode = 200;

  res.setHeader('Content-Type''application/json;charset=UTF-8');

  res.end('{"code":-3,"msg":"APPID不能为空","response":null,"systemDate":"2017-07-13"}');

});

server.listen(port, hostname, () => {

  console.log('Server running at http://'+hostname+':'+port);

});

使用fiddler查看报文如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

//请求报文如下:

OPTIONS http://192.168.1.6:3000/ HTTP/1.1

Host: 192.168.1.6:3000

Connection: keep-alive

Access-Control-Request-Method: GET

Origin: http://127.0.0.1:8080

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.98 Safari/537.36

Access-Control-Request-Headers: appid

Accept: */*

Referer: http://127.0.0.1:8080/dev/SVN_/h5Main/trunk/alipay/virtualCard20170509/src/pages/test/3-new.html

Accept-Encoding: gzip, deflate, sdch

Accept-Language: zh-CN,zh;q=0.8

 

//响应如下:

HTTP/1.1 200 OK

Content-Type: application/json;charset=UTF-8

Date: Fri, 14 Jul 2017 15:11:34 GMT

Connection: keep-alive

Content-Length: 79

{"code":-3,"msg":"APPID不能为空","response":null,"systemDate":"2017-07-13"}

可以看到通过在前端增加appid请求头,访问node js 搭建的服务器,浏览器触发了OPTIONS 预检验请求,但是服务端的响应头中没有设置Access-Control-Allow-Origin也没有允许OPTIONS请求,它却成功跨域拿到了json数据。发生这种情况首先想到的就是服务器环境不一致,我们后台使用的是java的spring mvc 肯定比node js 复杂千万倍。于是把这个问题反馈给后台同学,他们通过审查配置文件后发现,OPTIONS在java 的 spring MVC 框架中默认是禁止放行的。资料显示,框架认为这是不安全的行为,确实,不仅跨域还添加自定义请求头。于是开始改造后台响应报文,我配合后台一起调试。最后在原有基础上增加如下配置:

'Access-Control-Allow-Headers', 'appId' 来允许服务器请求中携带字段appId,如果还有其它字段,可以用逗号分隔填入;

'Access-Control-Allow-Methods',': POST, GET, OPTIONS'来允许服务器允许客户端使用 POST, GET 和 OPTIONS 方法发起请求;

添加完毕之后,响应头中增加对应字段,可以成功实现带自定义首部字段的跨域通信。

1

2

3

4

5

6

//响应头局部:

HTTP/1.1 200 OK

Content-Type: application/json;charset=UTF-8

Access-Control-Allow-Headers: appId

Access-Control-Allow-Methods: POST, GET, OPTIONS

Access-Control-Allow-Origin: *

但是我们发现每次这种情况都会触发OPTIONS请求,然后再去执行业务逻辑,虽然正常执行了,但是一个请求变成了两个,肯定增加了用户等待时间和服务器资源消耗,于是又在响应头中增加了Access-Control-Max-Age: 86400;表明该响应的有效时间为 86400 秒,也就是 24 小时。在有效时间内,浏览器无须为同一请求再次发起预检请求。浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。最后同一天内一个接口就只有一次OPTIONS请求啦,大功告成!

三:参考资料 stackoverfloww3 跨域文档HTTP访问控制

 码字很辛苦,转载请注明来自tuy博客《XMLHttpRequest 跨域时产生了 OPTIONS 请求》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值