跨域之Ajax
提到Ajax,一般都会想到XMLHttpRequest对象,通过这个对象向服务器发送请求,可以实现页面无刷新而更新数据。
由于同源策略的限制,一般情况下,只能通过XMLHttpRequest对象向同源的服务器发送请求,下面来看看向同源服务器发送请求的例子。
一、向同源服务器发送请求
例子的代码在这里 在本地测试一次成功的AJAX请求,把请求的详细信息贴出来:
以上的头部信息是将发送请求的页面在搭建的wamp环境下,使用http://localhost/my/ajax/gujia.html打开的,是在同源的情况下发送的请求,而且结果是成功的。
如果不通过localhost打开,而是直接本地打开,URL是file:///D:/wamp/www/my/ajax/gujia.html,结果是失败的,而且会报错,报错信息如下:
这就是不同源情况下发送请求产生的报错信息。
请求的头部信息如下:
对比同源情况下成功的请求,两者的头部信息的不同之处在与Request Headers下的Origin和Referer。
前者的Origin是http://localhost,后者是null;前者的Referer是在localhost服务器上的地址,也可以说是一个网址,而后者没有Referer。
那么如何才能在不同源的情况下,完成一次成功的Ajax请求呢?下面介绍。
二、CORS 跨域资源共享
CORS(Cross-Origin Resource Sharing) 是W3C推荐的一个让Web应用服务器能支持跨域访问控制,从而使得安全地进行跨域数据传输的标准。
该标准通过新增一系列 HTTP 头,让服务器能声明哪些来源可以通过浏览器访问该服务器上的资源。另外,对那些会对服务器数据造成破坏性影响的 HTTP 请求方法(特别是 GET 以外的 HTTP 方法,或者搭配某些MIME类型的POST请求),标准强烈要求浏览器必须先以 OPTIONS
请求方式发送一个预请求(preflight request),从而获知服务器端对跨源请求所支持 HTTP 方法。在确认服务器允许该跨源请求的情况下,以实际的 HTTP 请求方法发送那个真正的请求。服务器端也可以通知客户端,是不是需要随同请求一起发送信用信息(包括 Cookies 和 HTTP 认证相关数据)。
跨源资源共享标准( cross-origin sharing standard ) 使得以下场景可以使用跨站 HTTP 请求:
- 使用
XMLHttpRequest
发起跨域 HTTP 请求。 - Web 字体 (CSS 中通过
@font-face
使用跨域字体资源), 因此,网站就可以发布 TrueType 字体资源,并只允许已授权网站进行跨域调用。 - WebGL 贴图
- 使用 drawImage API 在 canvas 上画图
下面先把新增的一些HTTP头介绍一下。
1)Access-Control-Allow-Origin
返回的资源需要有一个 Access-Control-Allow-Origin 头信息,跨域时必需,语法如下:
1 Access-Control-Allow-Origin: <origin> | *
前面小节中失败的跨域请求的报错信息就是告诉我们服务器返回的响应头中缺少该参数,所以浏览器阻止了数据的获取。
<origin>参数指定一个允许向该服务器提交请求的URL。对于一个不带有Cookies的请求,可以指定为'*',表示允许来自所有域的请求。
2)Access-Control-Expose-Headers
该字段可选。设置浏览器允许访问的服务器的头信息的白名单,可以设置很多值,该头信息可选
举例:
1 Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
这样, X-My-Custom-Header
和 X-Another-Custom-Header这两个头信息,都可以被浏览器得到。
Access-Control-Expose-Headers
(可选) – 该项确定XmlHttpRequest
对象当中getResponseHeader()
方法所能获得的额外信息。通常情况下,getResponseHeader()
方法只能获得如下的信息:
Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma
通过Access-Control-Expose-Headers可以设置更多getResponseHeader()
方法能或得到的信息。
3)Access-Control-Max-Age
可选。这个头告诉我们这次预请求的结果的有效期是多久,如下:
1 Access-Control-Max-Age: <delta-seconds>
delta-seconds
参数表示,允许这个预请求的参数缓存的秒数,在此期间,不用发出另一条预检请求。
4)Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示服务器是否可以接收客户端发送来的Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true
,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true
,如果服务器不要浏览器发送Cookie,删除该字段即可
通过XMLHttpRequest对象的withCredentials属性来决定是否发送Cookie,设为true表示发送Cookie给服务器,false表示不发送。
1 var xhr = new XMLHttpRequest(); 2 xhr.withCredentials = true;
需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin
就不能设为*,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie
也无法读取服务器域名下的Cookie。
5)Access-Control-Allow-Methods
指明请求的可用方式有哪些(一个或者多个,不同方法之间用英文逗号隔开)。这个响应头信息作为对客户端发出的预检请求中Access-Control-Request-Method
的回复,必含。
1 Access-Control-Allow-Methods: <method>[, <method>]*
6)Access-Control-Allow-Headers
在响应预检请求的时候使用。用来指明在实际的请求中,可以使用哪些自定义HTTP请求头,那么在客户端发送请求时就可以加上这些可以使用的自定义请求头。
当预请求中包含Access-Control-Request-Headers
时必须包含。
7)Access-Control-Request-Headers
在发出预检请求时带有这个头信息,告诉服务器在实际请求时会携带的自定义头信息。如有多个,可以用逗号分开。
8)Access-Control-Request-Mrthod
在发出预检请求时带有这个头信息,告诉服务器在实际请求时会使用的请求方式。
9) Origin
表明发送请求或者预检请求的域,值是一个URI,告诉服务器端,请求来自哪里。它不包含任何路径信息,只是协议 + 域名 + 端口。Origin的值也可以是一个空字符串。
以上这几个头部信息前面6个是服务器端响应请求时要用到的,最后3个是前台浏览器发送跨域请求时要用到的。
对于跨域请求(CORS请求),可以分为两大类:简单请求和非简单请求。下面开始介绍。
2.1 简单请求
简单请求满足的条件:
(1) 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
(2)HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
(3)不使用自定义请求头
简单请求与同源时发送的请求差不多,不过在服务器端要明确Access-Control-Allow-Origin字段的值,以使请求的数据顺利返回客户端。
另外,客户端发现Ajax发送的是跨域请求,会在请求头中自动添加一个Origin字段。
下面是一个例子。
例子的代码还是在本地测试一次成功的AJAX请求得代码,仅仅是在例子中的php文件(即服务器端)中添加如下代码:
1 //设置响应头告诉浏览器支持跨域访问 2 header("Access-Control-Allow-Origin:*");
然后直接在本地打开发送请求的页面,不通过http://localhost,这次发现浏览器没有在报错,请求的数据顺利返回,头部信息如下:
对比前面报错的头部信息,仅仅是Response Headers(响应头)中多了我们添加到服务端的Access-Control-Allow-Origin:* 信息,从而通过Ajax实现了跨域请求。
这里Origin的值为null。
2.2 非简单请求
不是简单请求的请求就是非简单请求。
非简单请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
“预检请求”要求客户端必须先发送一个 OPTIONS 方式请求给目标服务器,来查明这个跨域请求对于目标服务器是不是安全可接受的。如果是才真正发送指定方式的请求。
下面看一个非简单请求的例子,例子的在线demo,点这里。
这个例子使用的是在线服务器的API接口,所以不知道后端的代码,只有前端自己写的代码:
html代码:
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge"> 6 <meta name="viewport" content="width=device-width, initial-scale=1"> 7 <title>Quote Machine</title> 8 9 </head> 10 <body> 11 <div id="fullpage"> 12 <div id="quote-machine"> 13 <div id="quote"> 14 <div id="quote-box"> 15 <blockquote id="quote-text"> 16 17 </blockquote> 18 <p id="author"></p> 19 </div> 20 21 <div id="btn-box"> 22 <div id="inner-btn-box"> 23 <button type="button" class="btn" id="tweet">Twitter</button> 24 25 <button type="button" class="btn" id="tumblr">Tumblr</button> 26 </div> 27 <button type="button" class="btn" id="new-quote-btn">New Quote</button> 28 </div> 29 </div> 30 <footer id="quote-footer">By Fogwind</footer> 31 </div> 32 </div> 33 </body> 34 </html>
CSS代码:
1 *{ 2 padding: 0; 3 margin: 0; 4 } 5 html,body{ 6 height: 100%; 7 font-family: Arial,Helvetica,sans-serif; 8 font-size: 12px; 9 line-height: 1.125; 10 } 11 #fullpage{ 12 height: 100%; 13 width: 100%; 14 15 display: flex; 16 justify-content: center; 17 align-items: center; 18 } 19 #quote-machine{ 20 width: 40%; 21 } 22 #quote-footer{ 23 text-align: center; 24 margin-top: 1em; 25 color: #fff; 26 } 27 #quote{ 28 border-radius: 5px; 29 background-color: #fff; 30 display: flex; 31 padding: 30px; 32 justify-content: space-between; 33 flex-direction: column; 34 } 35 #quote-box{ 36 width:100%; 37 margin-bottom: 1em; 38 39 } 40 #quote-text{ 41 font-size: 3em; 42 text-align: center; 43 margin-bottom: 1em; 44 } 45 #author{ 46 font-size: 1.5em; 47 text-align: right; 48 } 49 50 .btn{ 51 52 border: none; 53 color: #fff; 54 border-radius: 5px; 55 padding: 0.5em; 56 height: 30px; 57 } 58 59 #btn-box{ 60 display: flex; 61 justify-content: space-between; 62 } 63 #tweet{ 64 margin-right: 1em; 65 }
JS代码:
function addLoadEvent(func) { var oldonload=window.onload; if (typeof window.onload!='function') { window.onload=func; } else { window.onload=function() { oldonload(); func(); } } } function $(id) { return document.getElementById(id); } //封装一个创建XMLHttpRequest对象的函数,可以重复使用 function XMLHttp() { var xmlhttp; if(window.XMLHttpRequest) { xmlhttp = new window.XMLHttpRequest(); } else { //用来兼容以前的IE浏览器 xmlhttp = new ActiveXObject('Microsoft.XMLHTTP'); } return xmlhttp; } //下面这个函数用来打开一个请求和发送请求 function getQuote() { xmlhttp = XMLHttp(); if(xmlhttp) { //var url = "https://www.taobao.com/"; var url = 'https://andruxnet-random-famous-quotes.p.mashape.com/cat='; //绑定onreadystatechange事件 //JavaScript高级程序设计第三版上说在调用open()方法之前指定onreadystatechange事件能保证跨浏览器兼容性 xmlhttp.onreadystatechange = handle; //true表示异步机制 xmlhttp.open('get',url,true); xmlhttp.setRequestHeader("X-Mashape-Key","OivH71yd3tmshl9YKzFH7BTzBVRQp1RaKLajsnafgL2aPsfP9V");//Mashape application key xmlhttp.setRequestHeader('Content-Type','application/x-www-form-urlencoded'); xmlhttp.setRequestHeader("Accept","application/json"); //post方式的数据要在send()方法中发送,get方式向send()方法中传入null,数据在url中发送 xmlhttp.send(null); } } //指定回调函数 function handle() { //xmlhttp.readyState=4表示服务器响应完成,并且客户端已接受到全部响应数据,可以在客户端使用了 if(xmlhttp.readyState == 4) { //xmlhttp.status=200表示响应成功,等于304表示响应数据未发生修改可以使用浏览器缓存中的数据 //if语句的判断条件是照抄JavaScript高级程序设计第三版上的 if((xmlhttp.status >= 200 && xmlhttp.status < 300) || xmlhttp.status == 304) { //我这里返回的是JSON格式的字符串,用JSON对象的parse()方法把字符串转换成一个JavaScript对象 var quote = JSON.parse(xmlhttp.responseText); //alert(obj.author); $("quote-text").innerHTML = quote.quote; $("author").innerHTML = "--" + quote.author; currentQuote = quote.quote; currentAuthor = quote.author; } } } //改变背景颜色和字体颜色函数 function colorChange() { var random = Math.floor(Math.random() * 12); var colors = ['#16a085', '#27ae60', '#2c3e50', '#f39c12', '#e74c3c', '#9b59b6', '#FB6964', '#342224', "#472E32", "#BDBB99", "#77B1A9", "#73A857"]; document.body.style.color = colors[random]; $("fullpage").style.backgroundColor = colors[random]; $("tweet").style.backgroundColor = colors[random]; $("tweet").style.backgroundColor = colors[random]; $("tumblr").style.backgroundColor = colors[random]; $("new-quote-btn").style.backgroundColor = colors[random]; } function openURL(url) { window.open(url,'Share', 'width=550, height=400, toolbar=0, scrollbars=1 ,location=0 ,statusbar=0,menubar=0, resizable=0'); } window.onload = function() { getQuote(); colorChange(); } var newbtn = $("new-quote-btn"); newbtn.onclick = function() { getQuote(); colorChange(); }; var tweet = $("tweet"); var tumblr = $("tumblr"); var currentQuote, currentAuthor; tweet.onclick = function() { openURL('https://twitter.com/intent/tweet?hashtags=quotes&related=freecodecamp&text=' + encodeURIComponent('"' + currentQuote + '" ' + currentAuthor)); }; tumblr.onclick = function() { openURL('https://www.tumblr.com/widgets/share/tool?posttype=quote&tags=quotes,freecodecamp&caption='+encodeURIComponent(currentAuthor)+'&content=' + encodeURIComponent(currentQuote)); };
下面把请求的头部信息贴出来。
预检请求的:
预检请求的请求方式为 OPTIONS ,浏览器发送预检请求时添加了 Access-Control-Request-Method 和
Access-Control-Request-Headers 头信息。
这里Access-Control-Request-Method 为 GET ,是在JS代码中指定的请求方式。
Access-Control-Request-Headers为 x-masshape-key,也是在JS代码中指定的自定义的头信息,这里是使用这个公共API的APPKey的名字。
服务器响应预检请求的头信息中有Access-Control-Allow-Credentials头信息,值为true。有 Access-Control-Allow-Headers,值为允许前台发送请求时使用的自定义的头信息,这个例子中使用了x-masshape-key。有
Access-Control-Allow-Methods,表明服务器支持的所有跨域请求的方法。有
Access-Control-Expose-Headers。还有最重要的Access-Control-Allow-Origin,这里是null,与Orijin的值一样,可以说预检请求通过了,可以发送实际请求。
下面是实际请求的头信息:
一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS(跨域)请求,就都跟简单请求一样,会有一个Origin
头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin
头信息字段。
关于Cache-Control: no-cache看鸟哥的浏览器缓存机制。
关于CORS的参考文章:
(完)