浏览器基于安全(用户隐私)考虑,不允许不同域名的网站之间互相调用ajax,只是不允许ajax,其他的还是允许的。但是js.css也可以默认是阻止的。
同源策略(Same origin Policy)
浏览器出于安全方面的考虑,只允许与本域下的接口交互。不同源的客户端脚本在没有明确授权的情况下,不能读写对方的资源。
- 本域指的是?
- 同协议:如都是http或者https
- 同域名:如都是http://jirengu.com/a 和http://jirengu.com/b
- 同端口:如都是80端口(默认是80端口)
- 如:
http://jirengu.com/a/b.js 和 http://jirengu.com/index.php (同源)
- 不同源的例子:
http://jirengu.com/main.js 和 https://jirengu.com/a.php (协议不同)
http://jirengu.com/main.js 和 http://bbs.jirengu.com/a.php (域名不同,域名必须完全相同才可以)
http://jiengu.com/main.js 和 http://jirengu.com:8080/a.php (端口不同,第一个是80)
**需要注意的是: 对于当前页面来说页面存放的 JS 文件的域不重要,重要的是加载该 JS 页面所在什么域 **
如果发的请求的域名和当前页面的域名是相同的,那么请求是可以被浏览器接受的,是浏览器的操作,换句话说请求发出去之后,返回的响应浏览器是否愿意接受,这是浏览器的安全策略。
浏览器基于用户隐私的角度考虑,不允许不同域名的网站之间互相调用ajax。
(其他的默认情况是不阻止的)
页面向接口发送请求的时候,必须保证当前页面的URL和接口的URL是同源的。
什么是跨域
跨域就是当一个资源从与该资源的本身所在的服务器不同的协议、域名或者端口请求一个资源时,资源会发起一个跨域的HTTP请求。如http:www.baidu.com页面请求http://www.jianshu.com的资源
跨域的几种方法
- JSON
- CORS
JSONP和CPRS本质:不同域下的接口获取数据。 - 图像Ping:
动态创建图像经常用于图像Ping,图像Ping是与服务器进行简单、单向的跨域通信的一种方式。请求的数据是通过查询字符串的形式发送的,而响应可以是任意内容,但通常是素图或204响应,通过图像Ping,浏览器得不到任何具体的数据,但是可以通过监听load和error事件,知道响应是什么时候收到的。 - Comet:
Comet指的是一种更高级的Ajax技术,是一种从服务器向页面推送数据的技术,Comet能够让信息近乎实时地被推送到页面上。有两种实现Comet的方式:- 长轮询:页面发起一个到服务器的请求,然后服务器一致保持着连接打开,直到有数据可发送,发送完数据之后,浏览器关闭连接,随即又发起一个到服务器的新请求,这一过程在页面打开期间一直持续不断。
- HTTP流:在页面的整个生命周期内只使用一个HTTP连接,浏览器向服务器发送一个请求,而服务器保持连接打开,然后周期性的向浏览器发送数据。
- 服务器发送事件(SSE:Server-Sent Events)
SSE用于创建到服务器的单向连接,服务器通过这个连接可以发送任意数量的数据。具有两种实现方式:- SSE API
- 事件流
- Web Sockets
Web Sockets是一种与服务器进行全双工,双向通信的信道。Web Sockets不使用HTTP协议,而使用一种自定义的协议。(ws://或者wss://)这种协议专门为快速传输小数据设计。
JSONP
- 定义:
全称JSON with Padding(填充式JSON或参数式JSON),可用于解决主流浏览器的跨域数据访问的问题。JSONP有两部分组成:回调函数和数据。回调函数是当响应到来时应该在页面中调用的函数,回调函数的名字一般是在请求中指定,而数据就是传入回调函数中的JSON数据。
<script>的src可以引用任何网站的文件,当它引入一个接口的时候,接口返回的数据一般都是JSON格式的数据,这个数据是游离在页面上的,无法进行操作,所以直接用<script>标签引入是不可以的。
html中script标签可以引入其他域下的js,比如引入线上的jquery库。利用这个特性,可实现跨域访问接口。需要后端支持
echo $cb . '&&' . $cb . '(' . json_encode($ret) . ')';
- 定义数据处理函数_fun
- 创建script标签,src的地址执行后端接口,最后加个参数callback=_fun
- 服务端在收到请求后,解析参数,计算返还数据,输出 fun(data) 字符串。
- fun(data)会放到script标签做为js执行。此时会调用fun函数,将data做为参数。
- 原理:
利用<script>
标签没有跨域限制的“漏洞”来达到与第三方通信的目的。
当需要通讯时,本站脚本会创建一个<script>
元素,地址指向第三方的API地址,如<script src = "http://weather.com.cn?city=suzhou&callback=showWeather"></script>
,
并提供一个回调函数来接受数据(函数名可约定或通过地址参数传递)。第三方产生的响应为JSON格式的数据包装(所以称之为jsonp,即json padding),如weather({"suzhou":"rain"})
,这样浏览器会调用回调函数weather,并传递解析后的js对象作为参数,本站脚本可在callback函数里处理所传入的数据。用户只需要在加载之前定义好showWeather这个全局函数,在函数内处理参数即可。
<script>
function showWeather(ret){
console.log(ret)
}
</script>
<script src = "http://weather.com.cn?city=suzhou&callback=showWeather"></script>
http://weather.com.cn?city=suzhou接口要支持callback。
- 举例:
HTML代码部分:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>跨域</title>
<script src = "index1.js"></script>
</head>
<body>
</body>
</html>
js部分,文件名index1.js
//创建script标签,设置属性,将script标签添加到head中,
//至此,head标签中有两个script标签
function loadScript(url) {
var script = document.createElement('script')
script.src = url
script.type = "text/javascript"
var head = document.querySelector('head')
head.appendChild(script)
}
//设置回调函数的函数声明
function jsonpCallback(data) {
//把JSON数据序列化为JS对象
console.log(JSON.stringify(data))
}
//JSONP请求
loadScript('http://127.0.0.1:3000?callback=jsonpCallback')
后端逻辑
var url = require('url') // 获取url模块
var http = require('http')// 获取http模块
http.createServer(function(req,res){//创建服务器,并监听127.0.0.1的3000端口
//设置数据
var data = {
name: "lilili"
}
//url.parse()方法将一个url字符串解析为URL对象,
//.query方法获取的是不含问号的参数键值对,
//callback为键名,不固定,只要前后端约定好即可。
//所以callbackName为回调函数的名字,即index1.js文件中的jsonpCallback
var callbackName = url.parse(req.url,true).query.callback
//设置响应头为200,即OK状态,一般要提前约定好,不能乱写。
res.writeHead(200)
//res.end()即发送后端相应的结果${callbackName}为函数名,
//${JSON.stringify(data)}表示JSON字符串格式的实际参数,合起来为函数调用。
res.end(`${callbackName}(${JSON.stringify(data)})`)
}).listen(3000,'127.0.0.1')
//打印这句话只是方便在终端查看代码是否生效
console.log('启动服务器,监听 127.0.0.1:3000')
- 前端先设置好回调函数,并将函数名作为url的参数,将此url插入到新创建的script标签的src属性中,并将此script标签添加到页面的head中。
- 服务器端接受到的请求后,通过该参数获得回调函数名为jsonpCallback,并将js格式的数据作为函数调用的参数返回。
- 收到结果后因为是script标签,所以浏览器会当做脚本运行,从而达到跨域获取数据的目的。
- 验证
-
通过node index2.js在终端中启动服务器,监听端口3000,这件便建立了服务器
启动服务器.PNG
访问端口3000.PNG -
我们通过端口号的不同来模拟跨域的场景,打开另一个终端窗口,在页面目录下输入http-server
输入命令.PNG - 通过端口8080来访问刚才的页面,相当于开启两个监听不同端口的http服务器,通过页面中的请求来模拟跨域的场景。打开浏览器,访问
http://127.0.0.1:8080
就可以看到从http://127.0.0.1:3000
获取到的数据了
打开8080端口.PNG - 至此,通过JSONP跨域获取数据已经成功了。
-
- 总结:
JSONP是通过script标签加载数据的方式去获取数据当做JS代码来执行,提前在页面上声明一个函数,函数名通过接口传参的方式传给后台,后台解析到函数后在原始数据上包裹这个函数名,发送给前端。换句话说,JSONP需要对应的后端接口的配合才能实现。 - JSONP优点:
- 它不像XMLHttpRequest对象实现Ajax请求那样受到同源策略的限制;
- 兼容性好,在老的浏览器也能很好的运行;
- 不需要XMLHttpRequest或Active X的支持;并且在请求完毕后可以通过调用callback的方式回传结果。
- JSONP缺点:
- 只支持GET请求;
- 历史原因遗留的bug,注定被淘汰;
- 只支持跨域HTTP请求这种情况,不能解决不同域的两个页面或iframe之间进行数据通信的问题;
- 容易遭受XXS攻击,因为我们拿到的是对方接口的数据作为js执行,如果得到的是一个很危险的js,获取了用户信息和cookie,这时执行了js就会出现安全问题。
CORS
CORS 全称是跨域资源共享(Cross-Origin Resource Sharing),是一种 ajax 跨域请求资源的方式,支持现代浏览器,IE支持10以上。 实现方式很简单,当你使用 XMLHttpRequest 发送请求时,浏览器发现该请求不符合同源策略,会给该请求加一个请求头:Origin,后台进行一系列处理,如果确定接受请求则在返回结果中加入一个响应头:Access-Control-Allow-Origin; 浏览器判断该响应头中是否包含 Origin 的值,如果有则浏览器会处理响应,我们就可以拿到响应数据,如果不包含浏览器直接驳回,这时我们无法拿到响应数据。所以 CORS 的表象是让你觉得它与同源的 ajax 请求没啥区别,代码完全一样。
浏览器将CORS请求分为两类:简单请求和非简单请求
简单请求
- 请求方法是以下三种方法之一
- HEAD
- GET
- POST
- HTTP的头信息不超出以下几种字段
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
- 对于 简单请求,浏览器直接发出CORS请求,具体来说,就是在头信息之中,增加一个Origin字段。
示例:
GET /cors HTTP/1.1
Origin:http://api.bobo.com
Host:api.alice.com
Accept-Languague:en-us
Connection:keep-alive
User-Agent:Mozilla/5.0...
上面的头信息中,Origin字段用来说明本次请求来自哪个源(协议+域名+端口),服务器根据这个值决定是否同意这次请求。
如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP响应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意。这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
如果Origin指定的域名在许可的范围内,服务器返回的响应,会多出几个头信息字段。
Access-Control-Allow-Origin:http://api.bob.com
Access-Control-Allow-Credentials:true
Access-Control-Expose-Headers:FooBar
Content-Type:text/html;charset=utf-8
上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-
开头
(1)Access-Control-Allow-Origin
该字段是必须的,它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求
(2)Access-Control-Allow-Credentials
该字段可选,它的值是一个布尔值,表示是否允许发送Cookie,默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一般发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
(3)Access-Control-Expose-Headers
该字段可选,CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-type、Exoires、Last-Modifred、Pragma。如果想拿到掐字段,就必须在Access-Control-Expose-Headers里面指定,上面的例子指定getResponseHeder('FooBar')可以返回FooBar字段的值。
非简单请求
凡是不同时满足简单请求的两大条件的请求就是非简单请求。
非简单请求指对服务器有特殊要求的请求,比如请求方法是PUT或者DELETE,或者Content-Type字段的类型是application/json。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,成为“预检”请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就会报错。
下面一段浏览器的JavaScript脚本
var url = 'http://api.alice.com/cors'
var xhr = new XMLHttpRequest()
xhr.open('PUT',url,true)
xhr.setRequestHeader('X-Custom-Header','value')
xhr.send()
上面代码中,HTTP请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header.
浏览器发现,这是一个非简单请求,就会自动发出一个“预检”请求,要求服务器确认可以这样请求,下面是这个预检请求的HTTP头信息。
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
“预检”请求用的请求方法是OPTIONS,表示这个请求是用来询问的,头信息里面,关键字段是Origin,表示请求来自哪个源。除了Origin字段,“预检”请求的头信息包括两个特殊字段:
(1)Access-Control-Request-Method
该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上列是PUT。
(2)Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上列是X-Custom-Header。
服务器收到“预检”请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求的数据(该字段也可以设为*,表示同意任意跨源请求。)
如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。
XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
服务器回应的其他CORS相关字段如下。
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
(1)Access-Control-Allow-Methods
该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
(2)Access-Control-Allow-Headers
如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
(3)Access-Control-Allow-Credentials
该字段与简单请求时的含义相同。
(4)Access-Control-Max-Age
该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。
下面是"预检"请求之后,浏览器的正常CORS请求。
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
上面头信息的Origin字段是浏览器自动添加的。下面是服务器正常的回应。
Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8
上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。
CORS代码示例如下:
html部分:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>跨域</title>
<script src = "index1.js"></script>
</head>
<body>
</body>
</html>
JS部分
var xhr = new XMLHttpRequest() //1
xhr.open('GET','HTTP://127.0.0.1:3000',true)//2
xhr.onload =function(){
cosole.log(xhr.responseText)
}//4 因为onload是异步执行的,所以后执行
xhr.send()//3
这似乎和一次正常的异步ajax请求没什么区别,关键是在服务端接收到请求后的处理:
后端逻辑
var http = require('http')//获取http 模块
http.createServer(function(req,res){//创建服务器并监听127.0.0.1的3000端口
//设置响应头为200,即OK状态,一般也要提前约定好,不能乱写,设置允许访问的白名单。
res.writeHead(200,{
'Access-Control-Allow-Origin':'http://127.0.0.1:8080'
})
//res,send()即发送后端相应的结果。
res.send('hello,dolby')
}).listen(3000,'127.0.0.1')
console.log('启动服务器,监听 127.0.0.1:3000')
关键在于设置响应头中的Access-Control-Allow-Origin
,该值要与请求头中Origin一致才能生效,否则将跨域失败。
res.setHeader('Access-Control-Allow-Origin','http://localhost:8080')
主要观察Response Headers中的Access-Control-Allow-Origin和Request Headers的Origin。两者进行比较,如果一样,浏览器则可以请求成功,不一样,则失败。
与JSONP一样,开启两个服务器,并打开浏览器
两个服务器.PNG
成功的关键在于
Access-Control-Allow-Origin
是否包含请求页面的域名,如果不包含的话,浏览器将认为这是一次失败的异步请求,将会调用xhr.onerror中的函数。
- 如果
res.setHeaders('Access-Control-Allow-Origin'.'*')
,则表示任何人都可以使用这个数据。
CORS的优缺点:
- 优点:
- 使用简单方便,更为安全
- 支持POST请求方式
- 精确控制资源的访问权限
- 客户端无须增加额外的代码
- 缺点:
- CORS仅兼容IE10以上。
Server Proxy服务器
需要跨域的请求操作时发送请求给后端,让后端帮你代为请求,然后将获取的结果发送给你。
假设你的页面需要获取假设你的页面需要获取 CNode:Node.js专业中文社区 论坛上一些数据,如通过 https://cnodejs.org/api/v1/topics,因为不同域,所以你可以请求后端让其代为转发请求
const url = require('url');
const http = require('http');
const https = require('https');
const server = http.createServer((req, res) => {
const path = url.parse(req.url).path.slice(1);
if(path === 'topics') {
https.get('https://cnodejs.org/api/v1/topics', (resp) => {
let data = "";
resp.on('data', chunk => {
data += chunk;
});
resp.on('end', () => {
res.writeHead(200, {
'Content-Type': 'application/json; charset=utf-8'
});
res.end(data);
});
})
}
}).listen(3000, '127.0.0.1');
console.log('启动服务,监听 127.0.0.1:3000');
通过代码你可以看出,当你访问http://127.0.0.1:3000时,服务器收到请求,会代你发送请求https://cnodejs.org/api/v1/topics,最后将获取到的数据发送给浏览器。
-
启动服务器
启动服务器.PNG - 打开浏览器访问http://localhost:3000/topics就可以看到:
浏览结果.PNG
跨域成功!
网友评论