同源策略 跨域

187 阅读1分钟

同源策略

同源策略是浏览器最核心也最基本的安全功能,它指的是“协议,域名,端口”三者相同,即使两个不同的域名指向同一个IP地址,也非同源。

同源限制:ajax请求受到限制,不能获取DOM,不能获取cookie,localstorage,indexDB

跨域的方法

1 JSONP

利用 <script> 标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以。

原生实现

//原生实现
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
<script type="text/javascript">
    // 定义一个回调函数 处理相应数据
    var flightHandler = function(data){
        console.log(data)
    }
    // 要访问的网址 ?后面表示传递的参数以及回调函数名
    var url = "http://flightQuery.com/jsonp/flightResult.aspx?code=CA1998&callback=flightHandler";
    //创建一个标签
    var script = document.createElement("script")
    script.src = url;
    //将标签加入到DOM里面,调用开始
    document.head.appendChild(script) 
</script>
</head>

jQuery实现

 <script type="text/javascript">
   //回调函数可以直接用?,jquery自动处理
    $.getJSON('http://crossdomain.com/jsonp/services.php?callback=?', function (data) {
      console.log(data)
    })
  </script>

ajax实现

<script type="text/javascript">
  $(document).ready(function(){ 
    $.ajax({
      url: "http://flightQuery.com/jsonp/flightResult.aspx",
      dataType: "jsonp",
      data: "code=CA1998&callback=?", //传递的参数
  
      type: "get",   //一定是get方法 可省略
      jsonp: "callback", //用来获得回调函数名的参数名,可省略
      jsonpCallback: "flighthandler", //自定义传递的函数名 可省略

      success: function (json) {
        console.log(JSON.parse(json))
      },
      error: function () {
        console.log("fail")
      }
    })
  }
  </script>

vue实现

    this.$http.jsonp('http://flightQuery.com/jsonp/flightResult.aspx', {
      param: {},
      jsonp: "callback",
    }).then((res) => {
      console.log(res)
    })

后端node.js实现

    var querystring = require('querystring') //请求字符串
    var http = require('http') //http请求
    var sever = http.createSever() 
    sever.on('request', function (req, res) {  //服务器对request的响应
      var params = qs.parse(querystring.split("?")[1]) //取参数
      var fn = params.callback; //取回调函数名

      res.writeHead(200, { 'Content-Type': 'text/javascript' }) //设置响应头
      res.write(fn + '(' + JSON.stringify(params) + ')') //写响应体
      res.end() //响应内容发送
    })
    sever.listen('8080')

jsonp就是利用js脚本可以跨源访问的特点,进行一种hack,只能get请求。

2 CORS

需要浏览器和服务器同时支持cors,浏览器不需要配置,使用起来和ajax一样

两种请求

1 简单请求

同时满足下列两个条件:

(1) 使用下列方法:get,post,head

(2) 允许的首部字段;

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type (需要注意额外的限制)
  • DPR
  • Downlink
  • Save-Data
  • Viewport-Width
  • Width

其中content-type需要是下列之一:

  • text/plain (纯文本格式)
  • multipart/fram-data (post提交数据的方式)
  • application/x-www-form-urlencoded (post提交数据的方式)

(3) 请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。 (4)请求中没有使用 ReadableStream 对象。

2 非简单请求

非简单请求时会先发送预检请求 方法为OPTIONS

前提:假如站点 foo.example 的网页应用想要访问 bar.other 的资源。

请求头和响应头相关字段:

OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type


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://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

实际请求和响应

POST /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: http://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: http://foo.example
Pragma: no-cache
Cache-Control: no-cache

<?xml version="1.0"?><person><name>Arun</name></person>


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain

[Some GZIP'd payload]

响应首部

Access-Control-Allow-Origin //定了允许访问该资源的外域 URI。对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符,表示允许来自所有域的请求。当不为* 时,vary中必须包含Origin
Access-Control-Expose-Headers //响应头白名单,客户端可以获取
Access-Control-Max-Age  //预检结果被缓存的时间
Access-Control-Allow-Credentials  //允许携带cookie
Access-Control-Allow-Methods  //预检 允许的跨域请求方法
Access-Control-Allow-Headers  // 预检 允许请求中设置的首部字段

附带身份验证的请求

var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/credentialed-content/';
    
function callOtherDomain(){
  if(invocation) {
    invocation.open('GET', url, true);
    invocation.withCredentials = true;
    invocation.onreadystatechange = handler;
    invocation.send(); 
  }
}

默认情况下不带cookie,即跨域的cookie,在ajax中这是withCridentails为true,就会附带cookie,对于简单请求,cookie就被发送过去了,但是如果响应中不包含Access-Control-Allow-Credentials为true,浏览器就不会把响应的内容展示出来。另外对于附带身份凭证的请求,Access-Control-Allow-Origin不能设置为*,否则请求将会失败。

3 document.domain + iframe

对于ifram标签,跨域时显示内容没问题,但是操作数据不行

情景:两个页面的主域要相同

4 location.hash + iframe

5 window.name + iframe

原理:页面发生跳转,但是window.name依然存在。在iframe页面设置一个window.name,父页面能取到。

  <style>
    #ifra {
      height: 500px;
      width: 500px;
    }
  </style>
</head>

<body>
  <iframe id="ifra" src="http://jasonkid.github.io/fezone/" frameborder="0"></iframe>
  <script>
    let iframe = document.getElementById('ifra')

    iframe.onload = function () {
      iframe.onload = function () {   //第二次加载同源页面的时候,就可以取到ifram页面设置的window.name了
        let data = iframe.contentWindow.name 
        console.log(data)
      }
      iframe.src = 'about:blank' //设置为和父页面同源。页面发生跳转,但是window.name依然存在
    }
  </script>

</body>

6 navigation

7 postMessage

<body>
<iframe id="iframe" width="480px" height="270px" src="http://jasonkid.github.io/fezone/"></iframe>
<script>
    var iframe = document.getElementById('iframe');
    window.addEventListener('message', function(event) {
        console.log(event);
        event.source.postMessage('Message from parent page.', '*');
       //e.source是指发送这个消息的window对象,来自iframe里面的网页
       //e.origin = http://jasonkid.github.io/fezone/
    });
</script>
</body>

安全讨论

在HTML5之前,JSONP已经成为跨域的事实标准了,jQuery都给出了支持。 值得注意的是它只是Hack,并没有产生额外的安全问题。 因为JSONP要成功获取数据,需要跨域资源所在服务器的配合,比如资源所在服务器需要自愿地回调一个合适的函数,所以服务器仍然有能力控制资源的跨域访问。

跨域的正道还是要使用HTML5提供的CORS头字段以及window.postMessage, 可以支持POST, PUT等HTTP方法,从机制上解决跨域问题。 值得注意的是Access-Control-Allow-Origin头字段是资源所在服务器设置的, 访问控制的责任仍然是在提供资源的服务器一方,这和JSONP是一样的。