含义
1995年,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个策略。
最初,它的含义是指,A网页设置的 Cookie,B网页不能打开,除非这两个网页"同源"。所谓"同源"指的是"三个相同"。
-
协议相同
-
域名相同
-
端口相同
举例来说,http://www.example.com/dir/page.html
这个网址,协议是http://
,域名是www.example.com
,端口是80
(默认端口可以省略)。它的同源情况如下。
-
http://www.example.com/dir2/other.html
:同源 -
http://example.com/dir/other.html
:不同源(域名不同) -
http://v2.www.example.com/dir/other.html
:不同源(域名不同) -
http://www.example.com:81/dir/other.html
:不同源(端口不同)
同源的定义:如果两个页面的 “协议+域名+端口“ 都相同,则两个页面具有相同的源,就是同源。即便两个不同的域名指向同一个 IP 地址,也非同源。
目的
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。
同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
设想这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie,会发生什么?
很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。
由此可见,"同源政策"是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。
限制范围
随着互联网的发展,"同源政策"越来越严格。目前,如果非同源,共有三种行为受到限制。
(1) Cookie、LocalStorage 和 IndexDB 无法读取。
(2) DOM 无法获得。
(3) AJAX 请求不能发送。
虽然这些限制是必要的,但是有时很不方便,合理的用途也受到影响。下面,我将详细介绍,如何规避上面三种限制。
不受同源策略限制的
-
页面中的链接,重定向以及表单提交是不会受到同源策略限制的。
-
跨域资源的引入是可以的。但是JS不能读写加载的内容。如嵌入到页面中的
<script src="..."></script>,<img>,<link>,<iframe>
等。
AJAX跨域请求
为了保证用户安全,同源政策规定,AJAX请求只能发给同源的网址,否则就报错。浏览器的同源策略 禁止 页面发起跨域的ajax请求。
而所谓的跨域请求就是指:当前发起请求的域与该请求指向的资源所在的域不一样。这里的域指的是:协议 + 域名 + 端口号均相同,那么就是同域。
虽然在安全层面上同源限制是必要的,但有时同源策略会对我们的合理用途造成影响,为了避免开发的应用受到限制,有多种方式可以绕开同源策略,主要有CORS,JSONP、服务代理等。
-
CORS
-
JSONP
-
CORS
CORS
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
浏览器对于跨域ajax请求,都是浏览器自动完成,不需要用户参与,浏览器也不会拦截,而是向目标服务器发起了请求,然后在请求在响应头中寻找是否有access-control-allow-origin字段,并查看这个字段中的值是否包含本页面的来源,如果包含则不禁止本次请求,如果不包含,则禁止本次请求。
对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
get和post跨域
对于get和post请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin
字段。
下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin
字段。
GET /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com Accept-Language: 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
字段的值,要么是一个*
,表示接受任意域名的请求。
res.setHeader("Access-Control-Allow-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
、Expires
、Last-Modified
、Pragma
。如果想拿到其他字段,就必须在Access-Control-Expose-Headers
里面指定。上面的例子指定,getResponseHeader('FooBar')
可以返回FooBar
字段的值。
// 允许浏览器发送的头 res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization");
(4)Access-Control-Allow-Methods
允许客户端使用哪些请求方法
// 允许哪些请求方法 res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
withCredentials 属性
上面说到,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials
字段。
Access-Control-Allow-Credentials: true
另一方面,开发者必须在AJAX请求中打开withCredentials
属性。
var xhr = new XMLHttpRequest(); xhr.withCredentials = true;
否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。
但是,如果省略withCredentials
设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials
。
xhr.withCredentials = false;
需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin
就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie
也无法读取服务器域名下的Cookie。
与JSONP的比较
CORS与JSONP的使用目的相同,但是比JSONP更强大。
-
JSONP 只能实现 GET 请求,而 CORS 支持所有类型的 HTTP 请求
-
使用 CORS ,开发者可以是使用普通的 XMLHttpRequest 发起请求和获取数据,比起 JSONP 有更好的错误处理
-
JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
JSONP
JSONP 是一种非官方的跨域数据交互协议,是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。
它的基本思想是,网页通过添加一个<script>
元素,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。
原理:
-
JSONP 本质上是利用
<script><img><iframe>
等标签不受同源策略限制,可以从不同域加载并执行资源的特性,来实现数据跨域传输。 -
JSONP由两部分组成:回调函数和数据。回调函数是当响应到来时应该在页面中调用的函数,而数据就是传入回调函数中的JSON数据。
-
JSONP 的理念就是,与服务端约定好一个回调函数名,服务端接收到请求后,将返回一段 Javascript,在这段 Javascript 代码中调用了约定好的回调函数,并且将数据作为参数进行传递。当网页接收到这段 Javascript 代码后,就会执行这个回调函数,这时数据已经成功传输到客户端了。
首先,网页动态插入<script>
元素,由它向跨源网址发出请求。
// 原理展示:利用设置了src的script标签,浏览器会自动访问src连接的资源,没有跨域限制 let script = document.createElement('script'); document.body.appendChild(script); // 访问这个地址,就是发送请求,传递一个参数,callback,值为getinfo // 当src访问这个接口的时候,服务器返回了 getinfo(data) 字段,当这个script在dom中,会立即执行getinfo(data) 相当于调用getinfo函数 script.src = 'http://127.0.0.1:3000/info?callback=getinfo' // 调用getinfo的时候,得到从服务器传递的数据 function getinfo(data){ console.log(data);// {data: "获取info成功", error: 0} }
上面代码通过动态添加<script>
元素,向服务器http://127.0.0.1:3000
发出请求。注意,该请求的查询字符串有一个callback
参数,用来指定回调函数的名字,这对于JSONP是必需的。
由于<script>
元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了getinfo
函数,该函数就会立即调用。作为参数的JSON数据被视为JavaScript对象,而不是字符串,因此避免了使用JSON.parse
的步骤。
在服务端
app.get('/info', (req, res)=>{ // 原理:将callback参数的值当做函数名,和data组成一段js代码(调用这个函数)返回给客户端,客户端得到这一段代码会立即调用这个函数,调用函数时,把data传给客户端 // console.log(req.query);//{ callback: 'getinfo' } let data = JSON.stringify({error: 0, data: '获取info成功'});//要响应的数据 let code = `${req.query.callback}(${data})`;// getinfo(data)类似调用函数,把data当做参数传递 res.setHeader('Content-type', 'application/javascript;charset=utf-8');// 说明传递的是一段js代码 res.send(code); });
服务端设置jsonp数据还可以使用res.jsonp()
res.jsonp({error: 0, data: '获取info成功'});
客户端也有专门访问jsonp的请求,比如JQ的$.jsonp(),注意添加callback字段,值设置为?
$.getJSON('http://127.0.0.1:3000/info?callback=?', data => { console.log(data); //{data: "获取info成功", error: 0} });
JSONP优缺点:
JSONP 的优点是:它不像XMLHttpRequest
对象实现的Ajax请求那样受到同源策略的限制;它的兼容性更好,在更加古老的浏览器中都可以运行。
JSONP 的缺点是:它只支持 GET 请求,而不支持 POST 请求等其他类型的 HTTP 请求
PROXY 代理服务器
在服务端之前相互请求,也没有跨域限制,也没有方式的限制。
浏览器请求同源服务器,再由后者请求外部服务。
把客户端发送的请求一个服务器A(能正常访问的服务器,可以是请求源同源的服务器,或者设置了允许跨域请求的服务器),然后该服务器A把请求转发到外部服务器B,外部服务器B把响应结果再给到服务器A,服务器A再给客户端响应。
代理服务器适用于适用于有些服务端代码无法直接修改,如果服务器代码可以直接修改,直接设置Access-Control-Allow-Origin字段接好了。
比如:A域的页面向B域发起的ajax请求会被浏览器视为跨域请求,那么可以先让A域的页面向A服务器发起请求,然后A服务器收到这个请求之后,将请求转发给B服务器(服务器发送请求不存在跨域限制),从B服务器收到响应之后,再将这个响应回复给A域的页面。
比如快递100的接口:http://www.kuaidi100.com/query?type=yuantong&postid=805047801474183136
JSON和JSONP
虽然只有一个字母的差别,但其实他们根本不是一回事儿:
JSON是一种数据交换格式,而JSONP是一种依靠开发人员的聪明才智创造出的一种非官方跨域数据交互协议。
我们拿最近比较火的谍战片来打个比方,JSON是地下党们用来书写和交换情报的“暗号”,而JSONP则是把用暗号书写的情报传递给自己同志时使用的接头方式。
看到没?一个是描述信息的格式,一个是信息传递双方约定的方法。
一些接口
快递接口:http://www.kuaidi100.com/query
-
请求方法:GET
-
参数:
-
type:快递公司拼音 yuantong
-
postid:运单号 805047801474183136
-
天气预报:http://wthrcdn.etouch.cn/weather_mini
-
请求方法:GET
-
参数:
-
city:城市名
-
http://api.map.baidu.com/telematics/v3/weather?location=南京市&output=json&ak=WaOlVicc1qwOZbVieXGEZpG6qT9uTaWG&mcode=com.BaiduWeather&callback=?