详解跨域

1、跨域的概念

1.1、同源策略(same-origin policy,域名、协议、端口)

浏览器安全的基石是"同源政策",1995年,同源政策由 Netscape(网景) 公司引入浏览器,目前,所有浏览器都实行这个政策。同源策略指的是域名、协议、端口号都相同,只要 协议,域名,端口有任何一个的不同,就被当作是跨域。

为了保证用户信息的安全,防止恶意的网站窃取数据,目前,所有浏览器都实行了同源策略,要求域名、协议、端口必须都相同才属于同源,只有同源才可以访问其他页面的对象。

目前,如果非同源,共有以下三种行为受到限制:

  1. Cookie、LocalStorage 和 IndexDB 无法读取。
  2. DOM 无法获得。
  3.  AJAX 请求不能发送。

 

举例来说,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:不同源(端口不同)

 

URL由协议+主机名/域名+端口+路径+查询字符串+信息片段组成。

http://mail.163.com:8000/index.html 中,http://:是协议;mail:是服务器名;163.com:是域名;mail.163.com:这个是主机名(网站名),由服务器名+域名组成;8000:是端口号

 

1.2、为什么要限制跨域访问

同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。

设想这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie,会发生什么?

很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。由此可见,"同源政策"是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。

例子:比如一个黑客,他利用 iframe 把真正的银行登录页面嵌到他的页面上,当你使用真实的用户名和密码登录时,如果没有同源限制,他的页面就可以通过 JavaScript读取到你的表单中输入的内容,这样用户名和密码就轻松到手了。

虽然这些限制是必要的,但是有时很不方便,合理的用途也受到影响。比如有时公司内部有多个不同的子域,比如一个是location.company.com ,而应用是放在app.company.com , 这时想从 app.company.com去访问 location.company.com 的资源就属于跨域。

 

1.3、常见的出现跨域场景

  1. 两个跨域网页通过 iframe 嵌套。比如:http://www.baidu.com 通过 iframe 嵌入了 http://www.google.com 网页,此时如果通过 baidu 或者通过 google 来访问对方的数据,比如 dom 元素,或是全局变量,都会产生跨域问题
  2. 通过 window.open(url) 方法打开一个跨域网站。比如 http://www.baidu.com 通过 window.open() 方法打开了 http://www.google.com 网站,google 网站可以通过 widdow.opener 来获取 baidu 网站的引用,如果通过该引用来访问 baidu 的全局变量、dom元素等,就会产生跨域问题

 

1.4、跨域报错提示信息

跨域访问浏览器信息示例:

  • No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:9100' is therefore not allowed access. The response had HTTP status code 400.
  • Access to XMLHttpRequest at 'http://localhost:8080/hello' from origin 'http://parent.example.com:5500' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

 

2、解决cookie无法跨域访问的问题

2.1、设置 cookie  的域名(一级域名相同的情况)

Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。但是,如果两个网页的一级域名相同,只是二级或者三级等域名不同,可以通过设置 cookie 的 domain 为一级域名达到 cookie 共享的效果。

举例来说,父网页是http://w1.example.com/a.html,子网页是http://w2.example.com/b.html,只要添加 cookie 时,将 cookie 的域属性 domain 设置为 example.com,父子网页就能互相访问对方的 cookie。

示例:

子网页设置 cookie :

document.cookie="childname=childpage;domain=example.com";

父网页中即可读取到 cookie 的值

console.log(document.cookie);   //输出  childname=childpage

注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 无法通过这种方法。

cookie 的domain 属性默认是当前页面的域名,所以如果两个页面域名一样只是端口不一样时,此时cookie默认是可以共享的。

 

2.2、设置document.domain(一级域名相同的情况)

如果父子页面之间的一级域名相同想要实现跨域访问 cookie ,除了设置 cookie 的一级域名外,也可以通过设置 document.domain 来实现。使用 document.domain 来实现跨域访问,需要在子页面和父页面中同时设置 document.domain 的值为相同的一级域名值。

代码示例如下,假设父页面为:http://w1.example.com:5500/index.html,子页面为:http://w2.example.com:5501/index.html。

父页面代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>父页面</title>
</head>

<body>
    <h1>父页面</h1>

    <iframe src="http://w2.example.com:5501/index.html" frameborder="0" id="childIframe"></iframe>

    <script>
        document.domain = 'example.com';
        document.getElementById("childIframe").onload = function () {
            document.cookie = "parentname=parentpage" 
            console.log(document.cookie);
        }


    </script>
</body>

</html>

子页面如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>子页面</title>
</head>
<body>
    <h1>子页面</h1>

    <script>
        document.domain = 'example.com';
        document.cookie = "childname=childpage" 
    </script>
</body>
</html>

请注意,document.domain 该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。

(实测,在Chrome浏览器和IE浏览器上通过设置该属性也并不能实现cookie共享的效果,可能已经被废弃,所以不建议使用该特性)

 

2.2.1、document.domain 特性的基本介绍

默认情况下,document.domain 存放的是载入文档的服务器的主机名,可以手动设置这个属性,不过是有限制的,只能设置成当前域名或者上级的域名,并且必须要包含一个.号,也就是说不能直接设置成顶级域名。例如:id.qq.com,可以设置成qq.com,但是不能设置成com。

具有相同document.domain的页面,就相当于是处在同域名的服务器上,如果协议和端口号也是一致,那它们之间就可以跨域访问数据。 

 

3、解决iframe跨域问题

3.1、设置document.domain实现跨域访问dom元素(一级域名相同的情况)

如果父子页面之间的一级域名相同想要实现跨域访问 dom 元素 ,可以通过设置 document.domain 来实现。使用 document.domain 来实现跨域访问,需要在子页面和父页面中同时设置 document.domain 的值为相同的一级域名值。

写法可参考上面的 2.2。

(实测通过设置 document.domain 属性是可以实现跨域访问 dom 元素的)

 

3.2、window.postMessage 跨域传递消息

HTML5为了解决跨域问题,引入了一个全新的API:跨文档通信 API(Cross-document messaging)。这个API为window对象新增了一个window.postMessage方法,允许跨窗口通信,不论这两个窗口是否同源。

发送消息语法如下:

targetWindow.postMessage(message, targetOrigin, [transfer]);
  • targetWindow:目标窗口的引用,比如目标 iframe 的 contentWindow 属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。
  • message:将要发送到其他 window 的数据,可以是字符串或者对象、数组都行。该参数将会被结构化克隆算法序列化,这意味着你可以不受什么限制的将数据对象安全的传送给目标窗口而无需自己序列化。
  • targetOrigin:通过窗口的 origin 属性来指定哪些窗口能接收到消息事件,其值可以是字符串 "*"(表示无限制)或者一个 URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配 targetOrigin 提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。
    • 这个机制用来控制消息可以发送到哪些窗口;例如,当用 postMessage 传送密码时,这个参数就显得尤为重要,必须保证它的值与这条包含密码的信息的预期接受者的 origin 属性完全一致,来防止密码被恶意的第三方截获。如果你明确的知道消息应该发送到哪个窗口,那么请始终提供一个有确切值的 targetOrigin,而不是*。不提供确切的目标将导致数据泄露到任何对数据感兴趣的恶意站点。
  • transfer(可选):是一串和 message 同时传递的 Transferable 对象。这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

 

接收消息语法如下:

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

message事件的事件对象event,提供以下三个属性:

  • event.source:发送消息的窗口。(可以通过该属性来拿到父页面的引用)
  • event.origin: 消息发向的网址。(可以通过该属性来过滤一些不合法的消息来源地址)
  • event.data: 消息内容

 

使用示例:

假设父页面 http://parent.example.com:5500/index.html 和子窗口 http://child.example.com:5501/index.html 互相通信,写法如下:

父页面代码如下:

<body>
    <h1>父页面</h1>

    <iframe src="http://child.example.com:5501/index.html" frameborder="0" id="childIframe"></iframe>

    <script>
        window.onload = function() {
            var childIWin = document.getElementById('childIframe').contentWindow;
            childIWin.postMessage({txt: "hello,子页面"}, 'http://child.example.com:5501');
        }

        window.addEventListener('message', function (e) {  // 监听 message 事件
            console.log('父页面接收到数据:', e.source, e.origin, e.data);   //部分浏览器输出e.source可能有问题,所以可以不输出e.source
        });
    </script>
</body>

</html>

子页面代码如下:

<body>
    <h1>子页面</h1>

    <script>
        window.onload = function() {
            window.parent.postMessage("Hello,父页面", '*');
        }

        window.addEventListener('message', function (e) {  // 监听 message 事件
           console.log('子页面接收到数据:', e.source, e.origin, e.data);
        });
    </script>
</body>

</html>

打开父页面,可以看到输出如下:

可以看到,在父页面中可以接收到子页面通过 postMessage 传送的数据,而子页面中也可以接收到父页面通过 postMessage 传送的数据。

上面是通过 iframe 跨域时传送数据,实际上当通过 window.open() 打开跨域页面时也可以使用 postMessage 来跨域传送数据。通过 postMessage 也可以解决 LocalStorage 无法跨域访问的问题,只需通过 postMessage 传递 LocalStorage 数据即可。

 

3.3、通过片段识别符跨域传递消息

片段标识符(fragment identifier)指的是,URL的#号后面的部分,比如 http://child.example.com:5501/index.html#username=wen 中的 #username=wen。如果只是改变片段标识符,页面不会重新刷新。

父窗口可以把信息,写入子窗口的片段标识符,子窗口即可通过监听hashchange事件得到通知,并且通过获取 location.hash 值获取到父窗口传递的值。同样的,子窗口也可以改变父窗口的片段标识符。

代码示例:

父页面代码:

<body>
    <h1>父页面</h1>
    <iframe src="http://child.example.com:5501/index.html" frameborder="0" id="childIframe"></iframe>
    <script>
        setTimeout(() => {
            data = 'username=wen'
            var src = document.getElementById('childIframe').src + '#' + data;
            document.getElementById('childIframe').src = src;
        }, 3000);        
    </script>
</body>

子页面代码:

<body>
    <h1>子页面</h1>

    <script>
        window.onhashchange = checkMessage;
        function checkMessage() {
            var message = window.location.hash;
            console.log('hash值', message);
        }
    </script>
</body>

打开父页面,在 3 秒后可以看到子页面 iframe 的src 属性发生了改变,浏览器输出如下:

 

 

 

4、解决Ajax跨域发送的问题

同源政策规定,AJAX请求只能发给同源的网址,否则就报错。

 

4.1、通过JSONP实现跨域

JSONP是服务器与客户端跨源通信的常用方法,最大特点就是简单适用,老式浏览器全部支持,服务器改造比较小。

JSONP 是 JSON with padding(填充式 JSON 或参数式 JSON)的简写,是应用 JSON 的一种新方法。JSONP 看起来与 JSON 差不多,只不过是被包含在函数调用中的 JSON, 比如:

cbname({ "name": "wen" });   //cbname:回调函数名  JSONP可以看做就是一段以json为参数的函数调用代码

JSONP 由两部分组成:回调函数和数据,回调函数是当响应到来时应该在浏览器页面中调用的函数。

JSONP 是通过<script>标签来使用的,使用时可以为src 属性指定一个跨域 URL(这里的<script>元素与<img>元素类似,都有能力不受限制地从其他域加载资源)。因为 JSONP 是有效的 JavaScript 代码,所以在请求完成后,即在 JSONP 响应加载到页面中以后,就会立即执行。 jsonp 是需要服务器端进行配合设计的。

 

代码示例如下:

首先,网页动态插入<script>元素,由它向跨源网址发出请求,客户端代码如下:

function addScriptTag(src) {
    var script = document.createElement('script');
    script.setAttribute("type", "text/javascript");
    script.src = src;
    document.body.appendChild(script);
}

window.onload = function () {
    addScriptTag('http://localhost:8080/hello');
}

function foo(data) {
    console.log('ajax返回数据' + data);
};

服务器端代码如下:

public class HelloController {
    @RequestMapping("/hello")
    public String handle01(){
        return "foo('Hello, Spring Boot')";
    }
}

浏览器输出结果:

 

 

上面的代码可以写的更灵活一点,即前端将需要调用的方法名通过参数告诉给后端,后端通过解析前端的参数获取方法名,并且根据该方法名来动态拼接需要返回的字符串。

则前端代码如下:

window.onload = function () {
    addScriptTag('http://localhost:8080/hello?callbackname=foo');
}

服务器端代码如下:

public class HelloController {
    @RequestMapping("/hello")
    public String handle01(String callbackname){
        System.out.println("方法名:" + callbackname);
        return callbackname + "('Hello, Spring Boot')";
    }
}

 

JSONP的原理:通过 script 标签指向后端接口,script 标签会认为这是一个 js 文件,并且在加载完后会立即执行返回的 js 代码,而后端返回的字符串刚好就是一个函数,所以也就执行了该函数。jsonp 之所以能跨域,是因为他并不是发送ajax请求,并不是利用XMLHTTPRequest 对象和服务端进行通信,他其实是利用script标签,而script标签是没有同源策略限制的,可以跨域的。

 

JSONP的缺点:

  1. 它只支持GET请求而不支持POST等其它类型的HTTP请求
  2. 它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题。
  3. jsonp在调用失败的时候不会返回各种HTTP状态码。
  4. 缺点是安全性。万一假如提供jsonp的服务存在页面注入漏洞,即它返回的javascript的内容被人控制的。那么结果是什么?所有调用这个 jsonp的网站都会存在漏洞。于是无法把危险控制在一个域名下…所以在使用jsonp的时候必须要保证使用的jsonp服务必须是安全可信的。

 

4.1.1、jsonp解决跨域请求json文件的问题

前端开发在进行调试时,经常会使用一些模拟数据来模拟后端接口,此时我们可以通过请求本地 json 文件来模拟请求后端接口。

如果JS直接请求本地 json 文件会提示跨域问题,此时我们可以通过 jsonp 来解决这个问题:

//被请求的name.json文件应该这么写:
successCallback({
    "status": 'success',
    "data": [{
        "name": "mike"
    }]
})
<script>
    function successCallback(data) {
        console.log('模拟数据:', data);
    }
</script>

<!-- json文件中的函数名必须跟script标签中的回调函数、JS中定义的回调函数  名称保持一致 -->
<!-- <script src="./name.json?cb=successCallback"></script> -->

<script>
    var scriptDom = document.createElement('script');
    scriptDom.src = './name.json?cb=successCallback';
    document.body.appendChild(scriptDom);
</script>

输出:

 

4.2、CORS实现跨域

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。它通过服务器增加一个特殊的 Header[Access-Control-Allow-Origin] 来告诉客户端跨域的限制,如果浏览器支持CORS、并且判断Origin通过的话,就会允许XMLHttpRequest发起跨域请求。

CORS跨域资源共享,定义了必须在访问跨域资源时,浏览器与服务器应该如何沟通。

整个CORS通信过程由浏览器自动完成,对于前端开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。实现CORS通信的关键是服务器端,只要服务器实现了CORS接口,就可以跨源通信。如果服务器端实现了CORS,在进行AJAX请求时,浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求。

CORS背后的基本思想就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功还是失败。

服务器端对于CORS的支持,主要就是通过设置Access-Control-Allow-Origin来进行的。如果浏览器检测到相应的设置,就可以允许Ajax进行跨域的访问。只需要在后台中加上响应头来允许域请求!

CORS Header 如下:

  • Access-Control-Allow-Origin:必填字段,取值可以是请求时Origin字段的值,也可以是*,表示接受任意域名的请求。
  • Access-Control-Allow-Methods:必填字段,取值是逗号分隔的一个字符串,设置服务器支持的跨域请求的方法。注意为了避免多次OPTIONS请求,返回的是所有支持的方法,逗号分隔。
  • Access-Control-Max-Age:可选字段,用来指定预检请求的有效期,单位为秒,在此期间不用发出另一条预检请求,不指定时即使用默认值,Chrome默认5秒。常用浏览器有不同的最大值限制,Firefox上限是24小时 (即86400秒),Chrome 是10分钟(即600秒)。注意Access-Control-Max-Age设置针对完全一样的url,当url包含路径参数时,其中一个url的Access-Control-Max-Age设置对另一个url没有效果。
  • Access-Control-Allow-Headers:可选字段,CORS请求时默认支持6个基本字段,XMLHttpRequest.getResponseHeader()方法:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果需要支持其他Headers字段,必须在Access-Control-Allow-Headers里面指定。
  • Access-Control-Allow-Credentials:可选字段,布尔值类型,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中;如果设为true,即表示服务器允许在请求中包含Cookie,一起发给服务器。注意该值只能设为true,如果服务器不允许浏览器发送Cookie,删除该字段即可。

 

 示例,Nginx下的配置:

任意跨源请求都支持
 server {
        listen      8080;
        server_name  localhost;
        
        #*星号代表任意跨源请求都支持
        add_header Access-Control-Allow-Origin '*';  
        add_header Access-Control-Allow-Credentials "true";
        add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
        add_header Access-Control-Allow-Headers  'token,DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,XRequested-With';
        if ($request_method = 'OPTIONS') {
            return 200;
        }
    
      location /appmains {
            proxy_pass http://appmainsserver;
        }
}

前端代码跟一般ajax访问无区别,如下:

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
  if (xhr.readyState == 4 && xhr.status == 200) {
    console.log(xhr.responseText)
  }
}
xhr.open('GET', 'http://192.168.2.186:8081/KuayuAjax/server.php', true);
xhr.send(null);

 

5、Nginx解决跨域问题

通过 Nginx 的反向代理可以解决跨域问题。反向代理时,客户端对代理是无感知的,通过反向代理后,实际在浏览器看来,你访问的是同域的资源,也就不存在跨域的问题。

假设在父页面中需要跨域访问子页面的元素,父页面可通过 Nginx 访问,路径为:http://localhost/index.html,我们可以通过配置反向代理使得可访问该代理服务器来直接访问父页面,即父子页面在同一域名下,Nginx 配置如下:

server {
        listen       80;
        server_name  localhost;

        location /child/ {
            proxy_pass  http://child.example.com:5501/;  # 子页面路径
            index  index.html index.htm;
        }

        location / {
            root   F:/xxx/  # 父页面路径,如F:/VSCodeSpace/练习/JS练习/详解跨域/父页面/;
            index  index.html index.htm;
        }
}

父页面代码如下:

<body>
    <h1>父页面</h1>

    <!-- 子页面iframe -->
    <iframe src="http://localhost/child/index.html" frameborder="0" id="childIframe"></iframe>
    <script>
        document.getElementById('childIframe').onload = function() {
            var childIWin = document.getElementById('childIframe').contentWindow
            console.log('子页面', childIWin.location.href);
        }
    </script>
</body>

 

posted @ 2019-02-13 20:13  wenxuehai  阅读(566)  评论(0编辑  收藏  举报
//右下角添加目录