一:前言
对于跨域请求,一直没有采用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请求前端代码并没有发起,仔细查看请求头字段:
字段 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 |
|
通过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 |
|
使用fiddler查看报文如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
可以看到通过在前端增加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 |
|
但是我们发现每次这种情况都会触发OPTIONS请求,然后再去执行业务逻辑,虽然正常执行了,但是一个请求变成了两个,肯定增加了用户等待时间和服务器资源消耗,于是又在响应头中增加了Access-Control-Max-Age: 86400;表明该响应的有效时间为 86400 秒,也就是 24 小时。在有效时间内,浏览器无须为同一请求再次发起预检请求。浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。最后同一天内一个接口就只有一次OPTIONS请求啦,大功告成!
三:参考资料 stackoverflow、w3 跨域文档、HTTP访问控制
码字很辛苦,转载请注明来自tuy博客的《XMLHttpRequest 跨域时产生了 OPTIONS 请求》