前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >前端:跨域

前端:跨域

作者头像
WEBJ2EE
发布2020-05-22 17:50:21
1.2K0
发布2020-05-22 17:50:21
举报
文章被收录于专栏:WebJ2EEWebJ2EE

熟练掌握“跨域”问题的各种解决方案

是每位前端工程师的基本素养

代码语言:javascript
复制
1. 跨域基本原理
  1.1. 同源策略(Same origin policy)
  1.2. 同源策略限制了什么?
  1.3. 解决方案?
2. JSONP 跨域
3. postMessage 跨域
4. iframe 跨域
  4.1. iframe + iframe
  4.2. iframe + location.hash
5. CORS 跨域
  5.1. 定义
  5.2. 两种请求
  5.3. 简单请求
  5.4. 预检请求
  5.5. 身份凭证问题(Credentials)
  5.6. 响应头获取问题(response->headers)
  5.7. CORS 相关 HTTP 头部字段
6. Nginx 跨域
7. Websocket 跨域

1. 跨域基本原理

1.1. 同源策略(Same origin policy)

同源策略(same-origin policy)是一个重要的安全策略。它用于限制从一个源(origin)加载的文档或脚本,如何与另一个源(origin)的资源进行交互。

  • 如果两个 URL 的 protocol、port (如果存在)和 host 都相同的话,则这两个 URL 是同源。
  • IE 未将端口号纳入到同源策略的检查中。

1.2. 同源策略限制了什么?

示例1:跨域场景下,浏览器限制脚本API的访问。

page1.jsp

代码语言:javascript
复制
<%@ page language="java" contentType="text/html; charset=UTF-8"
  pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Insert title here</title>
  </head>
  <body>
    <h1>Page1</h1>
    <h3 id="address"></h3>
    <script>
      window.onload = function(){
        document.getElementById("address").innerText = location.href;
      }
</script>

    <button onclick="callPage2()">Call function in Page2!</button>
    <div>
      <iframe id="page2" 
        src="http://192.168.56.1:8180/cross-orgin/page2.jsp" 
        style="width:500px;"></iframe>
    </div>
    <script>
      function callPage2(){
        var page2 = document.getElementById("page2");
        page2.contentWindow.fnInPage2();
      }
</script>
  </body>
</html>

page2.jsp

代码语言:javascript
复制
<%@ page language="java" contentType="text/html; charset=UTF-8"
  pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Insert title here</title>
  </head>
  <body>
    <h1>Page2</h1>
    <h3 id="address"></h3>
    <script>
      window.onload = function(){
        document.getElementById("address").innerText = location.href;
      }
</script>

    <script>
      function fnInPage2(){
        alert("function in Page2 invoked!");
      }
</script>
  </body>
</html>

示例2:IE浏览器,跨域检查,不考虑端口。

1.3. 解决方案?

一个域下的文档或脚本试图去请求另一个域下的资源,这被称作为广义上跨域。我们经常讨论的跨域是从狭义角度去理解,即:由浏览器同源策略限制的一类请求场景。有以下几个常用的解决方案:

  • JSONP 跨域
  • postMessage 跨域
  • iframe 跨域
    • iframe + iframe
    • location.hash + iframe
  • CORS 跨域资源共享
  • Nginx 代理跨域
  • Websocket 协议跨域

2. JSONP 跨域

2.1. 原理

JSONP 利用了 <script> 标签可以加载跨域资源的特性,通过JavasScript Callback的形式实现跨域访问。具体来说,就是在 DOM 中通过动态创建 <script> 标签,并给标签设置 src 属性,在访问请求参数中传递需要回调的函数名;同时,服务端在响应 JSONP 请求时,将数据作为请求参数指定的客户端回调函数参数作为返回值。

2.2. 特性

优点:

  • 方案简单(只靠 <script> 标签就能实现);
  • 浏览器兼容性好,兼容所有浏览器;
  • 第三方库基本都支持(例如:jQuery);

缺点:

  • 只支持 GET,不支持 POST 等其它类型 HTTP 请求;
  • 只能解决跨域 HTTP 请求,不能解决跨域页面间 JS 调用;
  • JSONP 调用失败时,不会返回 HTTP 状态码;

2.3. 示例

jsonp.jsp:

代码语言:javascript
复制
<%@ page language="java" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html><head><meta charset="UTF-8"></head><body>
  <script type="text/javascript">
    function jsonp(url, params, callbackName) {
      var src = url + "?callbackName=" + callbackName;
      for(var key in params){
        var val = encodeURIComponent(params[key]);
        src += ("&" + key + "=" + val);
        }

        var script = document.createElement("script");
          script.src = src;

        document.getElementsByTagName("head")[0].appendChild(script);  
    }

    function mycallback(params){
      alert(params);
    }

    jsonp("http://192.168.56.1:8180/cross-orgin/HandleJSONP", {
      xm: "John",
      age: 18,
      idno: "37020320081206129X"
    }, "mycallback");
</script>
</body></html>

jsonp-jquery.jsp:

代码语言:javascript
复制
<%@ page language="java" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html><head><meta charset="UTF-8"></head><body>
  <script type="text/javascript" src="jquery-1.12.4.js"></script>
  <script type="text/javascript"> 
    function mycallback(params){
      alert(params);
    }

    $.ajax({
        url: 'http://192.168.56.1:8180/cross-orgin/HandleJSONP',
        dataType: "jsonp",
        data: {
          xm: "John",
          age: 18,
          idno: "37020320081206129X"
        },
        jsonp: "callbackName",
        jsonpCallback: "mycallback"
    })
</script>
</body></html>

HandleJSONP.java:

代码语言:javascript
复制
package webj2ee;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/HandleJSONP")
public class HandleJSONP extends HttpServlet {
  private static final long serialVersionUID = 1L;

  public HandleJSONP() {
    super();
  }

  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    doPost(request, response);
  }

  protected void doPost(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    String callbackName = request.getParameter("callbackName");

    String xm = request.getParameter("xm");
    String age = request.getParameter("age");
    String idno = request.getParameter("idno");

    System.out.println(xm);
    System.out.println(age);
    System.out.println(idno);

    response.getWriter().write(callbackName + "('Data Sent From Server!')");
  }
}

原理展示:

3. postMessage 跨域

3.1. 原理

window.postMessage() 方法可以安全地实现跨源通信,可用于:

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的iframe消息传递

API:

代码语言:javascript
复制
targetWindow.postMessage(message, targetOrigin);

兼容性:

3.2. 示例

postmessage-father.jsp

代码语言:javascript
复制
<%@ page language="java" contentType="text/html; charset=UTF-8"
  pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Insert title here</title>
  </head>
  <body>
    <h1>PostMessage-Father</h1>
    <h3 id="address"></h3>
    <script>
      window.onload = function(){
        document.getElementById("address").innerText = location.href;
      }
</script>
    <div>
      <iframe src="http://192.168.56.1:8180/cross-orgin/postmessage-child.jsp"
          style="width: 600px; height: 200px;"></iframe>
    </div>
    <script>
      window.addEventListener('message', function(e) {
        var origin = e.origin;
        var data = e.data;
        var source = e.source;

        console.log(origin, data);

        alert("Received message: "+data);
          }, false);
</script>
  </body>
</html>

postmessage-child.jsp

代码语言:javascript
复制
<%@ page language="java" contentType="text/html; charset=UTF-8"
  pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Insert title here</title>
  </head>
  <body>
    <h1>PostMessage-Child</h1>
    <h3 id="address"></h3>
    <script>
      window.onload = function(){
        document.getElementById("address").innerText = location.href;
      }
</script>
    <button onclick="postToFather()">postMessage to father window!</button>
    <script>
      function postToFather(){
        var data = {
                  name: '张三',
                  age: '18'
              };

        var targetWindow = window.parent;
        targetWindow.postMessage(
          JSON.stringify(data), 
          'http://localhost:8080'
        );
      }
</script>
  </body>
</html>

4. iframe 跨域

4.1. iframe + iframe

核心原理

A域 -> B域 -> A域 ==> parent.parent

parent.jsp:

代码语言:javascript
复制
<%@ page language="java" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html><head><meta charset="UTF-8"></head><body>

  <script type="text/javascript">  
  function fnParent(){
    alert('Parent function executed!');
  }  

  function callChild(){
    var iframe = document.getElementById("execIframe");
    iframe.src = "http://127.0.0.1:8181/crossdomain/iframe-crossorigin/childCaller.jsp?_=" + Math.random();
  }  
</script>  

  <h2>Parent(<%=request.getLocalAddr() + ":" + request.getLocalPort() %>)</h2>
  <p>
    <button onclick="callChild()">Call Child</button>
  </p>

  <iframe src="http://127.0.0.1:8181/crossdomain/iframe-crossorigin/child.jsp" 
        id="childFrame" name="childFrame" 
        width="500" height="200"></iframe>

  <iframe id="execIframe" src="" width="500" height="70"></iframe>
</body></html>

child.jsp:

代码语言:javascript
复制
<%@ page language="java" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html><head><meta charset="UTF-8"></head><body>
  <script type="text/javascript">    
  function callParent(){
    var iframe = document.getElementById("execIframe");
    iframe.src = "http://192.168.124.10:8080/crossdomain/iframe-crossorigin/parentCaller.jsp?_=" + Math.random();
  }

  function fnChild(){
    alert('Child function executed!');
  }  
</script>  

  <h2>Child(<%=request.getLocalAddr() + ":" + request.getLocalPort() %>)</h2>  

  <button onclick="callParent()">Call Parent</button>

  <iframe id="execIframe" src="" width="400" height="70"></iframe>
</body></html>

childCaller.jsp:

代码语言:javascript
复制
<%@ page language="java" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html><head><meta charset="UTF-8"></head><body>
  <script type="text/javascript">
    parent.window.childFrame.fnChild();
</script>  

  <h3>Child Caller(<%=request.getLocalAddr() + ":" + request.getLocalPort() %>)</h3>  
</body></html>

parentCaller.jsp:

代码语言:javascript
复制
<%@ page language="java" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html><head><meta charset="UTF-8"></head><body>
  <script type="text/javascript">
    parent.parent.fnParent();
</script>  

  <h3>Parent Caller(<%=request.getLocalAddr() + ":" + request.getLocalPort() %>)</h3>  
</body></html>

效果展示:

原理分析:父->子

原理分析:子->父

4.2. iframe + location.hash

URL有一部分被称为hash,就是#号及其后面的字符,它一般用于浏览器锚点定位,HTTP请求过程中不会携带hash,所以这部分的修改不会产生HTTP请求,但是会产生浏览器历史记录。此方法的原理就是改变 URL 的 hash 部分来进行双向通信。

  • 实现原理:a欲与b跨域相互通信,通过中间页c来实现。三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
  • 具体实现:A域:hash-parent.html -> B域:hash-child.html -> A域:hash-proxy.html。hash-parent 与 hash-child 不同域,可通过 hash 值单向通信;hash-child与hash-proxy也不同域,也通过 hash 单向通信;但hash-proxy 与hash-parent 同域,所以 hash-proxy 可通过 parent.parent 访问 hash-parent 页面所有对象。

hash-parent.jsp:

代码语言:javascript
复制
<%@ page language="java" contentType="text/html; charset=UTF-8"
  pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Insert title here</title>
  </head>
  <body>
    <h1>Hash-Parent</h1>
    <h3 id="address"></h3>
    <script>
      window.onload = function(){
        document.getElementById("address").innerText = location.href;
      }
</script>
    <button onclick="sendMsgToChild()">send message to child!</button>
    <div>
      <iframe id="iframe" 
          src="http://192.168.56.1:8180/cross-orgin/hash-child.jsp"
          style="width: 700px; height: 400px;"></iframe>
    </div>
    <script>
      function sendMsgToChild(){
        var iframe = document.getElementById('iframe');
        iframe.src = iframe.src + '#a_message_to_child';
      }

      function onMsgReceived(res) {
          alert('Parent received: ' + res);
      }
</script>
  </body>
</html>

hash-child.jsp:

代码语言:javascript
复制
<%@ page language="java" contentType="text/html; charset=UTF-8"
  pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Insert title here</title>
  </head>
  <body>
    <h1>Hash-Child</h1>
    <h3 id="address"></h3>
    <script>
      window.onload = function(){
        document.getElementById("address").innerText = location.href;
      }
</script>
    <div>
      <iframe id="iframe" 
          src="http://localhost:8080/cross-orgin/hash-proxy.jsp"
          style="width: 600px; height: 200px;"></iframe>
    </div>
    <script>
        var iframe = document.getElementById('iframe');

        // 监听 hash-parent.jsp 传来的 hash 值,再传给 hash-proxy.html
        window.onhashchange = function () {
          var hash = location.hash;

          alert("Child received: "+hash);

            iframe.src = iframe.src + "#a_message_from_child";
        };
</script>
  </body>
</html>

hash-proxy.jsp:

代码语言:javascript
复制
<%@ page language="java" contentType="text/html; charset=UTF-8"
  pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Insert title here</title>
  </head>
  <body>
    <h1>Hash-Proxy</h1>
    <h3 id="address"></h3>
    <script>
      window.onload = function(){
        document.getElementById("address").innerText = location.href;
      }
    </script>
    <script>
        // 监听hash-child.html传来的hash值
        window.onhashchange = function () {
          var hash = location.hash;

            // 再通过操作同域hash-parent.jsp的js回调,将结果传回
            window.parent.parent.onMsgReceived(hash);
        };
</script>
  </body>
</html>

5. CORS 跨域

5.1. 定义

Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP headers to tell browsers to give a web application running at one origin, access to selected resources from a different origin. A web application executes a cross-origin HTTP request when it requests a resource that has a different origin (domain, protocol, or port) from its own.

5.2. 两种请求

  • 简单请求(Simple requests)
  • 预检请求(Preflighted requests)

5.3. 简单请求

5.3.1 什么情况下是简单请求?

满足下面所有条件的请求,就是“简单请求”:

5.3.2 基本流程?

5.4. 预检请求

5.4.1 为什么要预检?基本流程是什么?

  • 非简单请求,是对服务器又特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是Application/json
  • 非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为预检请求
  • 浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用那些HTTP动词和头信息。只要得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则会报错

5.4.2 基本流程?

5.4.3 预检请求缓存

Access-Control-Max-Age gives the value in seconds for how long the response to the preflight request can be cached for without sending another preflight request.

代码语言:javascript
复制
Access-Control-Max-Age: <delta-seconds>

5.5. 身份凭证问题(Credentials)

  • 跨域场景下,XMLHttpRequest 默认不发送 Cookie,可通过 withCredentials 属性控制它发送 Cookie;
    • Access-Control-Allow-Origin,不能指定为“*”。
    • 对于简单请求,如果响应头中不包含“Access-Control-Allow-Credentials: true”,浏览器将拒绝此响应。
    • 对于预检请求,Access-Control-Allow-Credentials 它指定了实际的请求是否可以使用credentials
代码语言:javascript
复制
Access-Control-Allow-Credentials: true

5.6. 响应头获取问题(response->headers)

在跨域访问时,XMLHttpRequest 对象的 getResponseHeader() 方法只能拿到一些最基本的响应头:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果要访问其他头(例如:Content-Disposition),则需要在服务器端设置 Access-Control-Expose-Headers 响应头,允许浏览器访问其他响应头。

代码语言:javascript
复制
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header

5.7. CORS 相关 HTTP 头部字段

5.7.1 请求头部字段

代码语言:javascript
复制
Origin: <origin>
  • Origin 首部字段表明预检请求或实际请求的源站。
  • 所有访问控制请求(Access control request)中,都包含 Origin 请求头。
代码语言:javascript
复制
Access-Control-Request-Method: <method>
  • Access-Control-Request-Method 首部字段用于预检请求。用于将实际请求所使用的 HTTP 方法告诉服务器。
代码语言:javascript
复制
Access-Control-Request-Headers: <field-name>[, <field-name>]*

  • Access-Control-Request-Headers 首部字段用于预检请求。用于将实际请求所携带的首部字段告诉服务器。

5.7.2 响应头部字段

代码语言:javascript
复制
Access-Control-Allow-Origin: <origin> | *
  • origin 参数的值指定了允许访问该资源的外域 URI。
  • 对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符,表示允许来自所有域的请求。
代码语言:javascript
复制
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
Access-Control-Max-Age: <delta-seconds>
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: <method>[, <method>]* // 用于预检请求的响应
Access-Control-Allow-Headers: <field-name>[, <field-name>]* // 用于预检请求的响应

6. Nginx 跨域

7. Websocket 跨域

WebSockets 是一种先进的技术。它可以在用户的浏览器和服务器之间打开交互式通信会话。使用此API,您可以向服务器发送消息并接收事件驱动的响应,而无需通过轮询服务器的方式以获得响应。

WebSocket 支持跨域通信

参考:

The Web Origin Concept: https://tools.ietf.org/html/rfc6454 Same-origin policy: https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy Same-origin policy: https://web.dev/same-origin-policy/ Window.postMessage(): https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage WebSockets: https://developer.mozilla.org/zh-CN/docs/Web/API/WebSockets_API HTTP访问控制(CORS): https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS Cross-Origin Resource Sharing: https://www.w3.org/TR/cors/ XDomainRequest - Restrictions, Limitations and Workarounds: https://docs.microsoft.com/en-us/archive/blogs/ieinternals/xdomainrequest-restrictions-limitations-and-workarounds

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-05-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 WebJ2EE 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档