同源策略与跨域

sleep技术讨论区 2019-06-28

同源策略

同源策略same origin policy中的重要内容就是URL(uniform resource locator),统一资源定位符,俗称网址。URL中的resource资源就是css,js,html,img等内容。
同源策略与跨域

origin

源包括当前页面的域名、协议、端口号。http协议默认端口是80,https协议默认端口是443。同源策略是浏览器的一个安全机制功能,Same Origin Policy,同源就是当协议、域名、端口号一致时就是同源。不同源的客户端脚本在没有明确授权下,不能读写对方的资源。简单地理解就是因为同源策略的限制,它是浏览器为了安全性考虑一种非常重要的策略,a.com 域名下的js无法操作b.com或是c.a.com域名下的对象。更详细的说明可以看下表:
同源策略与跨域

同源策略的意义

浏览器基于用户的隐私安全目的,防止恶意网站窃取数据(只是浏览器有这个同源策略设置,但是用命令行curl请求某个跨域地址时能得到相应结果),不允许不同域名的网站之间互相调用ajax XHR对象,只是不允许XHR对象,对其他的图片、js脚本、css脚本还是可以通过标签跨域调用。所以css/js/img可以跨域请求(即引用),AJAX不能请求跨域的资源。
curl http://www.abc.com 用这个命令获得了http://www.abc.com指向的页面,同样,如果这里的URL指向的是一个文件或者一幅图都可以直接下载到本地。如果下载的是HTML文档,那么缺省的将不显示文件头部,即HTML文档的header。要全部显示,请加参数 -i,要只显示头部,用参数 -I。任何时候,可以使用 -v 命令看curl是怎样工作的,它向服务器发送的所有命令都会显示出来。为了断点续传,可以使用-r参数来指定传输范围。

同源策略会限制以下三种行为

【1】 Cookie、LocalStorage 和 IndexedDB 无法读取。Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。
【2】 DOM 无法获得。
【3】 AJAX 请求无效(可以发送,但浏览器会拒绝接受响应)。

主域名和子域名

主域名 abc.com
www.abc.com //www是子域名
bbs.abc.com //bbs是子域名
beijing.bbs.abc.com //beijing.bbs是子域名
haidian.beijing.bbs.abc.com //haidian.beijing.bbs是子域名
主域名是不带www的域名,例如a.com,主域名前面带前缀的通常都为二级域名或多级域名,例如www.a.com其实是二级域名。

跨域

解决跨域问题的两个前提特别注意:
第一,如果是协议和端口造成的跨域问题“前端”无法解决。
第二:在跨域问题上,域仅仅是通过“URL的首部”来识别而不会去尝试判断相同的ip地址对应着两个域或两个域是否在同一个ip上。比如在host文件中可将两个不同域名绑定到同一个IP地址上形成跨域。
“URL的首部”可在console.log控制台中用window.location.方法名获取

跨域访问的四种方式

跨域就是用某种方法突破同源策略的限制,实现获取其他域中的资源。实现跨域一般有四种方法:

1.JSONP实现跨域

JSONP(json with padding)方式, 通过script标签请求资源,允许用户在scr地址中传递一个callback参数(callback=abc)即将预先定义好的回调函数名以查询字符串的形式传递给服务端,服务端收到请求后会将要返回的数据用这个callback参数(abc)包裹住再返回,即将请求传入的参数abc作为函数名来包裹住要返回的JSON数据,比如abc(JSON),这样客户端在收到服务端返回的abc(JSON)文件后默认用JS解析执行。通过JSONP就可以随意定制自己的函数来自动处理返回数据了。

jsonp的原理:虽然浏览器默认禁止了跨域访问,但并不禁止在页面中script标签引用其他域下的JS文件,比如线上jquery库,并可以自由执行引入的JS文件中的function(包括操作cookie、Dom等等)。根据这一点,可以方便地通过动态创建script节点的方法来实现完全跨域的通信。例如a.com/index.html中可以引用b.com/main.js、b.com/style.css、b.com/logo.png等资源,此类操作不受同源策略限制。实际操作中如果在a.com下用ajax去请求(读写)b.com下的内容会被同源策略阻止,但a.com里如果引用了b.com/main.js,虽然可以引用,但当这个引用的js文件(在a.com下引用)去读写(ajax)b.com的资源时一样会提示ajax错误。注意读写和引用有本质区别的,受同源策略限制ajax不能(POST写/GET读)请求跨域内容,但可以通过script引用的方式获取目标域上js文件,如果在这个被引用的JS文件内存放数据,这样就能从目标域获取到数据了,这就是JSONP实现的原理。
JSONP与JSON没有关系。JSON是规定语法的一种字符串的写法。JSONP(json with padding),这里的padding就是被请求的目标域B域返回的数据其外层包裹的A域预先定义好的函数+括号。JSONP就是动态的script,即A域前台传什么callback=abc给B域名后台,B域就生成对应的abc方法,这个方法的执行过程是A域预先定义好的
jsonp的缺点:
1.安全问题,src引用是开放的,所以jsonp的资源都被所有人访问到。解决方法是用jsonp中的token参数,通过A域和B域共用同一套cookie来验证A的身份。
2.只能用GET方式不能用POST方式获取数据即只能读不能写,因为是基于scr引用的,引用是get请求。
3.可被注入恶意代码如?callback=alert(1); 这问题只能用正则过滤字符串的方法解决,过滤callback后的内容不能有括号之类的条件

JSONP的实现代码如下:
1.定义数据处理函数

appendHtml(){
    xxxxx
}

2.创建script标签,src的地址执行后端接口,最后加个参数callback=appendHtml.如:

var script=document.createElement('script')
script.src="http://127.0.0.1/getNews?callback=appendHtml"

3.服务端在收到请求后,解析参数,计算返还数据,输出 appendHtml(data) 字符串。
4.前台页面收到服务端返回的appendHtml(data)字符串所构建的script标签,页面加载这个script标签时做为js执行。此时会调用appendHtml()函数,将data做为参数。
注意:JSONP实现的前提是后端必须有JSONP的API接口,即后端有将前端传入的参数作为函数名包裹数据返回js文件的逻辑。如:

var data=[{"a":1,"b":2}]
var cb=req.query.callback;
if(cb){
    res.send(`${cb}(JSON.stringify(${data}))`)
}else{
    res.send(`${data}`)
}
<script>//动态创建script标签
function abc(){
var script=document.createElement('script')
script.setAttribute("type","text/javascript")
script.src='//b.com/data.js?callback=xxx'
document.body.appendChild(script)
document.body.removeChild(script)//插入标签且加载数据实现后再删除,更简洁
}
abc()//声明后再调用,动态获取src引用的资源
</script>

需后台配合,代码如下图
同源策略与跨域

封装一个JSONP函数

function jsonp(url,_callback){
        var scriptNode=document.createElement("script")
        scriptNode.setAttribute("type","text/javascript")
        scriptNode.setAttribute("src",`${url}?callback=${_callback}`)
        document.head.appendChild(scriptNode)
    }
            
    function getMusic(content){ /*定义数据返回时所执行的回调函数*/
        console.log(content.song[0])
        var comeMusic=content.song[0],
            musicTitle=comeMusic.title,
            musicArtist=comeMusic.artist,
            imgSrc=comeMusic.picture,
            _src=imgSrc.match(/.[^@]*/g)[0] /*正则匹配返回的图片地址*/
            console.log(_src)
    
            imgNode.src=_src
            h3Node.innerText=musicTitle
            h6Node.innerText=musicArtist
    }

调用jsonp函数:
jsonp(http://www.aaa.com,getMusic)

2.CORS 跨域资源共享 Cross-Origin Resource Sharing

CORS允许浏览器向跨域服务器发出XMLHttpRequest请求,突破了AJAX只能同源使用的限制。CORS需要浏览器和服务器同时支持,目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。CORS原理:浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。只要服务器实现了CORS接口,就可以跨源通信。
以nodejs server-mock后台为例:
res.header("Accept-Control-Allow-Origin","http://www.a.com:8080")//只允许http://www.a.com:8080这个源发起的请求
res.header("Accept-Control-Allow-Origin","*")//允许所有源发起的请求

浏览器将跨域请求分为两类
简单请求(simple request),同时满足以下两个条件
【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

非简单请求(not-so-simple request),不满足以上两个条件的请求就是非简单请求
AJAX发起跨域请求时,如果是简单类型请求,请求头信息如下:
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指定的源,不在服务端许可范围内,服务器会返回一个正常的HTTP回应。但这个响应头信息没有包含Access-Control-Allow-Origin字段。浏览器接收后就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
如果Origin指定的域名在许可范围内,服务器返回的响应,响应头会包含以下信息字段
Access-Control-Allow-Origin: http://api.bob.com //允许来自源http://api.bob.com的访问,如果是*则代表允许来自所有源的访问
Access-Control-Allow-Credentials: true //是否允许客户端在请求中发送Cookie
Access-Control-Expose-Headers: FooBar //允许CORS请求拿到除默认的Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma外的其他字段。这里允许拿到foobar字段信息。
Content-Type: text/html; charset=utf-8

CORS请求默认不包含Cookie信息,如果需要包含Cookie信息,一方面要服务器同意
Access-Control-Allow-Credentials: true //但Access-Control-Allow-origin 不能是*
另一方面,开发者必须在AJAX请求中打开withCredentials属性。
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
此时Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

AJAX发起跨域请求时,如果是非简单类型请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json,非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为“预检”请求,浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
比如js代码如下:
var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true); //方法是PUT
xhr.setRequestHeader('X-Custom-Header', 'value'); //自定义请求头信息
xhr.send();
请求头信息如下:
OPTIONS /cors HTTP/1.1 //方法是OPTIONS,表示这个请求是用来询问的。
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...
如果服务器否定了“预检”请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。浏览器接收后就知道出错了,被XMLHttpRequest对象的onerror回调函数捕获。
如果服务器接受了“预检”请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。服务器发送响应头信息如下:
HTTP/1.1 200 OK
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

JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

3.HTML5跨文档通信API window.postMessage

HTML5中最酷的新功能之一就是 跨文档消息传输Cross Document Messaging。下一代浏览器都将支持这个功能:Chrome 2.0+、Internet Explorer 8.0+, Firefox 3.0+, Opera 9.6+, 和 Safari 4.0+ 。 Facebook已经使用了这个功能,用postMessage支持基于web的实时消息传递。

举例,父窗口aaa.com向子窗口bbb.com发消息,调用postMessage方法如下:

var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');

postMessage方法的第一个参数是具体的信息内容(支持任意类型),第二个参数是接收消息目标窗口的源origin(协议 + 域名 + 端口)也可以设为*,表示不限制域名,向所有窗口发送。
子窗口向父窗口发送消息的写法如下:
window.opener.postMessage('Nice to see you', 'http://aaa.com');
父窗口和子窗口都可以通过message事件,监听对方的消息:

window.addEventListener('message', function(event) {
  console.log(e.data);
},false);

事件对象event的三个属性:
event.source:发送消息的窗口对象,对发送消息的窗口对象的引用;
event.origin: 发送消息的窗口的源(协议、域名、端口号)这里不是接受消息的窗口的源
event.data: 消息内容
event.origin属性可以过滤不是发给本窗口的消息,举例如下:
当bbb网站收到来自aaa网站发来的消息

window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
  if (event.origin !== 'http://aaa.com') return;//判断发消息的窗口的源是否是aaa网站的源,这里的event.origin和postMessage()方法中的origin不一样!!
  if (event.data === 'Hello World') {
      event.source.postMessage('Hello', event.origin);//这里event.origin指向aaa网站的源即消息接收的窗口的源
  } else {
    console.log(event.data);
  }
}

otherWindow.postMessage方法中,otherWindow是其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。即只能适用于open方法打开的页面相互发送消息,或者通过iframe嵌套的页面之间发送消息。通过window.postMessage()方法还可读取其他窗口的LocalStorage
window.open("http://www.bbb.com:8881/b_index.html","title") //主页面中打开子页面
window.opener.postMessage(${msg},"http://www.aaa.com:8888") //引用打开子页面的主页面

4.降域 实现iframe窗口跨域访问 实现不同子域页面cookies共享

当两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置document.domain来实现iframe窗口相互访问或设置cookie。降域方式只适用于同一站点下不同子域名共享cookie或者iframe页面中嵌套的子域名页面之间的访问。降域不适用访问LocalStorage 和 IndexedDB,postMessage()方法可访问LocalStorage
用改写document.domain+iframe的方法来获取目标域数据。缺点是安全性差,一个页面被攻击后另一个页面的数据也会被泄露且不支持ajax方式请求数据。降域只能解决主域相同而二级域名(子域名)不同的两个页面请求数据的情况,如果把script.a.com的domian设为alibaba.com那显然是会报错。domain只能设置为主域名,不可以在b.a.com中将domain设置为c.a.com;且只能由子域名改到父域名或父父域名,不能从父父域名改到子域名(比如将b.com改成script.b.com是不行的)

举例来说,A网页是http://w1.example.com/a.html,B网页是http://w2.example.com/b.html,子域名不同默认情况下会被同源策略阻止访问。只要将两个页面都设置相同的document.domain,两个网页就可以共享Cookie或在iframe窗口下相互访问。
document.domain = 'example.com';
服务器也可以在设置 Cookie 的时候,指定 Cookie 的所属域名为一级域名以后二级域名和三级域名不用做任何设置,都可以读取这个Cookie。比如.example.com。
Set-Cookie: key=value; domain=.example.com; path=/

降域的特点:只能对主域相同子域不同的iframe页面和父页面(或者open方法打开的主域相同的页面)形式的跨域起作用,可实现的两个页面共享cookie。修改domain时top页面和iframe页面都要使用document.domain去修改成一致的域名。具体的做法是可以在http://www.a.com/a.html和http://script.a.com/b.html两个文件中分别加上document.domain = 'a.com';然后通过a.html文件中创建一个iframe,去控制iframe的contentDocument,这样两个js文件之间就可以“交互”了。验证步骤如下:
1.本地文件夹中有两个文件index.html(www.a.com下的网页),b.html(script.a.com下的网页),a.com下的index.html页面中有iframe页面(src="http://script.a.com:8080/b.html")
2.由于域仅仅是通过“URL的首部”来识别,不会判断两个不同域名是否为同一IP地址。根据这点,修改本地host文件增加两行,子域名不同,主域名相同来模拟跨域。
127.0.0.1 www.a.com
127.0.0.1 script.a.com
3.用mock start命令启动服务器并分别访问
http://www.a.com:8080/index.html
http://script.a.com:8080/b.html
两个页面console.log(document.domain)时分别返回www.a.com和script.a.com,此时是跨域状态。
4.当在www.a.com下的html页面中执行以下脚本时提示
var ccc=document.getElementsByTagName("iframe")[0].contentDocument
同源策略与跨域
(注:.contentDocument方法可操作iframe页面内的信息)
5.解决方案:www.a.com下的index.html页面和script.a.com下的b.html页面中都增加脚本,将两个页面的域设置成相同的主域名

<script>
document.domain="a.com"
</script>

再次执行var ccc=document.getElementsByTagName("iframe")[0].contentDocument
返回的就是b.html文件中的信息

相关推荐

sunlizhen / 0评论 2016-03-16