3
头图

Preface

This article is an enhanced version "Those Coquettish Cross-domain Operations" written by the author in 2008.
Gu Yun learned the new from the past. After reviewing the articles of the year, he still felt that there were a lot of deficiencies and omissions, and the depth of thinking was still lacking, so he reproduced.
I have limited personal abilities, and I welcome criticism and corrections.

Name rectification and apology

Start with a bow first.
Tucao about translation, browse all kinds of Wikis and RFCs in English names are "cross-origin", origin should be translated into "source", "source", the combination of accurate translation is "cross-origin". But somehow it became "cross-domain" in the Chinese area. This also caused a lot of misunderstanding. The relationship between domian and origin is mentioned below. How much does this messy translation make? Mengxin confuses these two concepts (including me).
Therefore, this article will only use the accurate translation of "cross-source", and I apologize for the error in my previous article.

Demo case

The most important part of this remake is that the author has implemented a complete set of demonstration cases, with front-end and back-end codes, no third-party dependencies on the front-end, Express-based server, full source code details, and a perfect combination of theory and practice. All cross-source solutions described below can be demonstrated locally, and can be played with NodeJS environment without complicated configuration, compilation and container. Portal
Homepage screenshot:
首页

Same-Origin Policy

In 1995, the same-origin policy was introduced into browsers by Netscape. Currently, all browsers implement this security policy.
The "cross-source" mentioned in this article is to bypass the restrictions of this strategy under the premise of ensuring security.

Core idea

The purpose of the same-origin policy is to ensure that the files (resources) provided by different sources are independent of each other, similar to the concept of a sandbox. In other words, there are no restrictions only when different file scripts are provided by the same source. Restrictions can be subdivided into two areas:

  • Object access restriction
    Mainly reflected in the iframe, if the parent and child pages belong to different sources, there will be the following restrictions:

    • The DOM (Document Object Model) cannot be accessed each other, that is, the document node cannot be obtained. The methods and attributes of the document mounted under the document, including all its child nodes, cannot be accessed. This is also the reason why Cookies follow the same-origin policy, because document.cookie cannot be accessed.
    • For the BOM, there are only a small amount of permissions, which means that you can obtain window objects from each other, but all methods and most attributes are unavailable (such as window.localStorage , window.name etc.), and only a few attributes can be accessed with limited access, such as the following two:

      • Readable, window.length .
      • Writable, window.location.href .
  • Network access restriction
    It is mainly reflected in Ajax requests. If the target source of the initiated request is different from the source of the current page, the browser will have the following restrictions:

    • Interception Response: For simple request , the browser will initiate a request, the server normal response, but as long as the server returns a response header does not meet the requirements, it will ignore all data returned directly to error.
    • Restricted requests: For non-simple requests, modern browsers will first initiate pre-check request , but as long as the response header returned by the server does not meet the requirements, it will directly report an error and will not initiate a formal request. In other words, this In this case, the server cannot get any data related to this request.

What is Same-Origin

Origin is strictly defined in the Web domain and consists of three parts: protocol, domain, and port.

origin = scheme + domain + port

That is to say, these three are all the same, so they can be called the same.
For example, suppose there is a http://example.com source is 060b5a1c2e8d18, and a request is made to the following source, and the result is as follows:

origin(URL)resultreason
http://example.comsuccessThe protocol, domain and port number are the same (the browser defaults to port 80)
http://example.com:8080failDifferent ports
https://example.comfailDifferent agreements
http://sub.example.comfailDifferent domain names

Cross-Origin Solution (Cross-Origin)

The era when the same-origin policy was proposed was the era when the traditional MVC architecture (jsp, asp) prevailed. At that time, most of the pages were filled by server rendering, and developers would not maintain independent API services, so in fact, the cross-source requirements are Relatively few.
In the new era, the separation of front-end and back-end and the rise of third-party JSSDK have only begun to discover that although this strategy greatly improves the security of the browser, it is sometimes inconvenient and its reasonable use is also affected. such as:

  • The independent API service uses an independent domain name for the convenience of management;
  • Front-end developers need to use remote APIs for local debugging;
  • JSSDK developed by a third party needs to be embedded in someone else’s page for use;
  • Open API for public platforms.

Since then, cross-source solutions to solve these problems have been proposed. It can be described as a hundred schools of thought. Among them, there are many amazing operations. Although there are now standard CORS solutions, it is worth learning to understand the interaction between the browser and the server. of.

JSON-P (self-filling JSON)

JSON-P is one of the most popular cross-source solutions. Now it will be used in some environments that need to be compatible with old browsers. The famous jQuery also encapsulates its methods.

principle

Please don't know the name. The P in the name means padding, which means "padding". This method does not use ordinary json formatted text in the communication process, but "JavaScript script with its own padding function".
How to understand "JavaScript script with built-in filling function"? Take a look at the example below.
Assuming that there is this getAnimal function on the global (Window), and then introducing a script that calls this function and passes in data through the script tag, cross-source communication can be realized.

// 全局上有这个函数
function getAnimal(data){
  // 取得数据
  var animal = data.name
  // do someting
}

Another script:

// 调用函数
getAnimal({
  name: 'cat'
})

That is to say, the feature that the browser will automatically run when the JavaScript script is introduced can be used to pass data to the global function. If you use this script that calls the function as the output of the server-side API, you can use this to achieve cross-source communication. This is the core principle of the JSON-P method, which is filled with the data of the global function.

Process

  1. Define the callback function globally, which is the function to be called in the js script output by the server API;
  2. Create a new script tag, src is the API address, insert the tag into the page, and the browser will initiate a GET request;
  3. The server generates a js script according to the request and returns;
  4. When the page waits for the script tag to be ready, it will automatically call the globally defined callback function to get the data.
[PS] Not only the script tag, all tags that can use the src attribute can initiate GET requests without being restricted by the same-origin policy (CSP is not configured), such as img, object, etc., but only the script tag can automatically run js code .

JSONP

Error handling

  • The front end can capture network errors through the error event of the script tag, but the specific cause of the error is not known, that is, the response status code of the server cannot be obtained;
  • If the script returned by the server runs incorrectly, the front end can only capture it through the global error event.

Practical tips

  • front end

    • In order to avoid polluting the whole world, it is not recommended that the front-end generate a large number of global functions with random names. You can use one object to save all callback functions, and give each callback function a unique id. Only the unified executor is exposed globally, and the callback function is called by the id;
    • If you follow the previous advice, the callback function in the global object needs to be cleaned up in time;
    • A new script tag is generated for each request, which should be cleaned up in time after completion;
    • For flexibility, it can also be agreed with the server to pass the callback function name as a parameter to reserve the expansion space for multiple global objects.
  • Server

    • Only need to receive the request of GET method, other methods can be judged as illegal;
    • Parameters can only be obtained in the requested URL, such as query or path;
    • The content-type of the response message header is set to text/javascript;
    • It is strongly recommended to close the HTTP protocol cache to avoid data inconsistencies. For the method, refer to the author's article on 160b5a1c2e9493 HTTP;
    • The returned script is written into the response message body in plain text format. Since the script is run directly, special attention should be paid to XSS attacks.

The front-end "one object saves all callback functions" design idea:

function initJSONPCallback() {
  // 保存回调对象的对象
  const cbStore = {}
  // 这里形成了一个闭包,只能用特定方法操作 cbStore。
  return {
    // 统一执行器(函数)。
    run: function (statusCode, data) {
      const { callbackId, msg } = data
      try {
        // 运行失败分支。
        if (...) {
          cbStore[callbackId].reject(new Error(...))
          return
        }
        // 运行成功分支。
        cbStore[callbackId].resolve(...)
      } finally {
        // 执行清理。
        delete cbStore[callbackId]
      }
    },
    // 设置回调对象,发起请求时调用。
    set: function (callbackId, resolve, reject) {
      // 回调对象包含成功和失败两个分支函数。
      cbStore[callbackId] = {
        resolve,
        reject
      }
    },
    // 删除回调对象,清理时调用。
    del: function (callbackId) {
      delete cbStore[callbackId]
    }
  }
}

// 初始化
const JSONPCallback = initJSONPCallback()
// 全局暴露执行器,这也是 API 返回脚本调用的函数。
window.JSONPCb = JSONPCallback.run

For the specific code, please refer to the demo case JSONP part source code.

to sum up

  • advantage

    • Simple and fast, it is indeed faster than the solution that requires iframe (you will know by experiencing it in the demo case);
    • Support ancient browsers (IE8-);
    • No domain requirements, can be used for third-party APIs.
  • Disadvantage

    • It can only be the GET method, cannot customize the request header, and cannot write the request body;
    • The amount of requested data is limited by the maximum length of the URL (different browsers vary);
    • Debugging is difficult, the specific cause of server error cannot be detected;
    • Special interface support is required, and standard API specifications cannot be used.

SubHostProxy (subdomain proxy)

Subdomain proxy is a very practical cross-origin solution under specific environmental conditions. It can provide an experience that is no different from normal Ajax requests.

principle

First figure out what is a subdomain (domain)? The resolution of the domain name is from right to left. When we apply for the domain name, we apply for the two rightmost paragraphs (with dots as segments), and the following parts can be customized for the owner. You can add a few more paragraphs. Yes, these derived domains are subdomains. For example, api.demo.com is a demo.com .
Theoretically, the two examples in the example are different domains. According to the domain mentioned above, they are part of the origin, so they are also different sources. However, the browser allows you to change the document domain of the page to the parent of the current domain, that is, api.demo.com page run the following code can be changed demo.com , but this change only affects the rights document of Ajax is no effect.

// 在 api.demo.com 页面写如下代码
document.domain = 'demo.com'
[PS] document.domain : It can only be set once; only the domain part can be changed, but the port number and protocol of the page cannot be modified; the port of the current page will be reset to the protocol default port (ie 80 or 433); it only works for the document , Does not affect the same-origin strategy of other objects.

Therefore, the principle of this solution is to use this method to enable the parent page to have access to the document of the subdomain page. The subdomain happens to be the domain of the API, and then the request is initiated through the subdomain page proxy to achieve cross-origin communication.

Process

Assume that the domain of the server API is api.demo.com , and the page domain is demo.com . They are running under the http protocol and the port is 80.

  1. Deploy a proxy page under the subdomain, set its domain to demo.com , and can include tools to initiate Ajax (jQuery, Axios, etc.);
  2. The main page also sets the domain to demo.com ;
  3. Create an iframe tag on the main page to link to the proxy page;
  4. When the proxy page in the iframe is ready, the parent page can use iframe.contentWindow obtain control of the proxy page and use it to initiate Ajax requests.

SubHostProxy

Error handling

  • The error event of the iframe is invalid in most browsers (default), so the error in the iframe is unknown to the main page;
  • Through the load event of the iframe, you can check whether the proxy page is loaded to indirectly determine whether there is a network error, but the specific cause of the error is not known, that is, the response status code of the server cannot be obtained;
  • When the main page obtains control of the proxy page, the error handling is no different from normal Ajax.

Practice tips

  • front end

    • It takes time to load the proxy page (in fact, it is slow), so pay attention to the timing of initiating the request, so as not to request before the proxy page is loaded;
    • It is not necessary to load a new proxy page for every request. It is strongly recommended to keep only one and share multiple requests;
    • If you follow the recommendations in the previous article, you also need to consider the failure of the proxy page to load, to avoid failure after one failure;
    • You can use preloading to load the proxy page in advance to avoid increasing the request time;
    • The main page must use the document.domain setting, that is, the current domain has met the requirements, that is to say, although the domain of the current page is xxx , it still has to call document.domain='xxx' again.
  • Server

    • Only use standard 80 (http) or 443 (https) port deployment (or use reverse proxy);
    • The domain of the proxy page must be consistent with the domain of the API, and have a common parent with the domain of the homepage (or the domain of the main page is the parent);
    • Theoretically, the proxy page only needs to be document.domain=xxx , so it can be as concise as possible.

Design ideas for sharing iframes:

// 将创建 iframe 用 promise 封装,并保存起来。
let initSubHostProxyPromise = null

// 每次请求之前都应先调用这个函数。
function initSubHostProxy() {
  if (initSubHostProxyPromise != null) {
    // 如果 promise 已经存在,则直接返回,由于这个 promise 已经 resolve,其实就相当于返回了已有的 iframe。
    return initSubHostProxyPromise
  }
  // 没有则重新创建。
  initSubHostProxyPromise = new Promise((resolve, reject) => {
    const iframe = document.createElement('iframe')
    // 填入代理页地址。
    iframe.src = '...'
    iframe.onload = function (event) {
      // 这是一种 hack 的检测错误的方法,见演示案例 README 。
      if (event.target.contentWindow.length === 0) {
        // 失败分支
        reject(new Error(...))
        setTimeout(() => {
          // 清理掉失败的 promise,这样下次就会重新创建。
          initSubHostProxyPromise = null
          // 这里还需移除 iframe。
          document.body.removeChild(iframe)
        })
        return
      }
      // 成功分支,返回 iframe DOM 对象。
      resolve(iframe)
    }
    document.body.appendChild(iframe)
  })
  return initSubHostProxyPromise
}

Please refer to the specific presentation case SubHostProxy part source.

to sum up

  • advantage

    • Any type of request can be sent;
    • Standard API specifications can be used;
    • Can provide an experience that is indistinguishable from normal Ajax requests;
    • Convenient and accurate error capture (except for iframe network errors);
    • Supports ancient browsers (IE8-).
  • Disadvantage

    • There are strict requirements for the domain and cannot be used for third-party APIs;
    • iframe has a greater impact on browser performance;
    • Cannot use non-protocol default port.

HTML-P/MockForm (self-filling HTML/mock form)

This solution is generally called "simulated form" on the Internet, but I don't think it is accurate. Using a form to initiate a request is not its core feature (there are also several solutions used later), and its core should be "self-filling HTML ".

principle

I call it HTML-P to borrow the name of JSON-P, and its idea is very similar to the JSON-P solution. The server-side API returns a js script that can be automatically run for data filling, and it will directly return the entire HTML page. No way.
But in fact, HTML still has restrictions on data filling. The first is the restriction of the same origin. If the parent and child pages are of different origins, they cannot access each other. The natural solution is to document.domain mentioned in the "subdomain proxy", but its The purpose is just the opposite of "subdomain proxy". By modifying the domain of the document, the subpage can obtain the access authority of the main page, so as to fill the data of the main page and realize cross-origin communication.

// API 返回包含如下脚本的 HTML ,就可访问父级页面的全局函数进行数据填充。
document.domain = 'xxx'
window.parent.callbackFunction(...)

As for the role of the form, it actually uses the target attribute of the form. When the form is submitted, it will make the iframe of the specified name jump. The jump is actually to initiate a request. Therefore, the request method natively supported by the browser form component can be used. To use, it is precisely because the form is used to initiate the request, the server API must return a text in HTML format.

Process

Assume that the domain of the server API is api.demo.com , and the page domain is demo.com . They are running under the http protocol and the port is 80.

  1. Define the callback function globally, which is the function to be called in the HTML output by the server API;
  2. The main page setting domain is demo.com ;
  3. Create an iframe tag on the main page and specify the name;
  4. Create a new form tag, specify target as the name of the iframe just now, and add data and configuration requests;
  5. Submit the form and jump in the iframe;
  6. The server receives the request, generates and returns an HTML page according to the request parameters, and its domain is set to demo.com ;
  7. The iframe completes the loading of HTML, the child page calls the callback function defined globally on the main page, and the main page gets the data.

MockForm

Error handling

  • The error event of the iframe is invalid in most browsers (default), so the error in the iframe is unknown to the main page;
  • Through the load event of the iframe, you can check whether the proxy page is loaded to indirectly determine whether there is a network error, but the specific cause of the error is not known, that is, the response status code of the server cannot be obtained;
  • The error that occurs when the subpage calls the main page is an error within the iframe, so it is also unknowable.

Practice tips

  • front end

    • In order to avoid polluting the whole world, it is not recommended that the front-end generate a large number of global functions with random names. You can use one object to store all callback functions. For this, please refer to the above JSON-P;
    • The main page must use the document.domain setting, that is, the current domain has met the requirements.
    • Since the page in the iframe is different each time the request is made, the iframe tag can be reused, but the page cannot be reused;
    • When concurrent, multiple iframe pages will be generated at the same time, which will lead to extreme performance degradation. Concurrent scenarios are not suitable for this solution;
    • The form and iframe tags should be cleaned up in time after completion;
  • Server

For the specific code, please refer to the demo case MockForm part source code.

to sum up

This solution can be said to be a stitched version of "JSON-P" and "Subdomain Proxy", with both advantages and disadvantages inherited.

  • advantage

    • Any type of request can be sent (subject to browser form tag support);
    • Compared with "subdomain proxy", no proxy page is an advantage.
    • Supports ancient browsers (IE8-).
  • Disadvantage

    • There are strict requirements for the domain and cannot be used for third-party APIs;
    • Iframes have a great impact on browser performance, and multiple iframes are required for concurrency, which basically cannot be used in scenarios that require concurrency;
    • Cannot use non-protocol default port.
    • Error capture is difficult, the specific cause of server error cannot be detected, and operation error cannot be captured;
    • Special interface support is required, and standard API specifications cannot be used.

WindowName

This is a solution centered on the window.name feature.

principle

This solution takes advantage of the feature of window.name: once assigned, its value will not be changed when the window (iframe) is redirected to a new url. Although window.name still follows the same-origin policy, and only the same-origin can read the value, we only need to write the value on a non-same-origin page and then redirect to the same-origin page to read the value to achieve cross-origin communication.
The method of initiating a request is the same as that of "HTML-P", which is achieved by triggering an iframe jump with a form.

// 通过 iframe 的 load 事件取得 window.name 的值。
iframe.onload = function (event) {
  const res = event.target.contentWindow.name
}

Process

  1. Create an iframe tag on the main page and specify the name;
  2. Create a new form tag, specify target as the name of the iframe just now, and add data and configuration requests;
  3. Submit the form and jump in the iframe;
  4. The server receives the request, generates and returns an HTML page according to the request parameters;
  5. iframe loads HTML, runs the script to set the data to window.name, and redirects;
  6. The iframe loads the HTML again, and the load event is triggered when it is finished;
  7. The main page listens to the load event of the iframe and obtains the value of its window.name.

WindowName

Error handling

  • The error event of the iframe is invalid in most browsers (default), so the error in the iframe is unknown to the main page;
  • Through the load event of the iframe, you can check whether the proxy page is loaded (the non-same source requires a hack method) to indirectly determine whether there is a network error, but the specific cause of the error is not known, that is, the server's response status code cannot be obtained ;
  • The remaining errors can be captured normally.

Practice tips

  • front end

    • The related attention points of form and iframe are the same as "HTML-P";
    • In theory, the page redirected to the same domain does not need any content, as long as it has HTML format, it should be as simple as possible, and because it does not need to be changed, it can be cached for a long time;
    • Although theoretically the load event of the iframe will be triggered twice (once for a non-same-origin page and once for a same-origin page), in fact, as long as the load is redirected before triggering, the load event of the non-same-origin page will not be received;
    • Redirection should use window.location.replace , so that history will not be generated, which will affect the back operation of the main page;
    • For flexibility, it is recommended to pass the URL of the redirected page to the server.
  • Server

Please refer to the specific presentation case WindowName part source.

to sum up

  • advantage

    • Any type of request can be sent (subject to browser form tag support);
    • No domain requirements, can be used for third-party APIs;
    • Supports ancient browsers (IE8-).
  • Disadvantage

    • The iframe has a great impact on the browser performance, and the two jumps make it worse, and multiple iframes are required concurrently, which basically cannot be used in scenarios that require concurrency;
    • The almost blank same-origin redirection page can be said to be meaningless traffic, which affects traffic statistics;
    • Error capture is difficult, the specific cause of server error cannot be detected, and operation error cannot be captured;
    • Special interface support is required, and standard API specifications cannot be used.

WindowHash

This is a scheme centered on the hash part of the url.

principle

This solution takes advantage of window.location.hash : pages in different domains can be written and unreadable. And only changing the hash part (after the pound sign) will not cause the page to jump. That is, you can write the hash part of the main page url for non-same-origin subpages, and the main page can realize cross-source communication by monitoring hash changes.
The method of initiating a request is the same as that of "HTML-P", which is achieved by triggering an iframe jump with a form.

// 现代浏览器有 hashchange 事件可以监听。
window.addEventListener('hashchange', function () {
  // 读取 hash
  const hash = window.location.hash
  // 清理 hash
  if (hash && hash !== '#') {
    location.replace(url + '#')
  } else {
    return
  }
})
// 降级方案,循环读取 hash 进行“监听”。
var listener = function(){
    // 读取 hash
    var hash = window.location.hash
    // 清理 hash
    if (hash && hash !== '#') {
      location.replace(url + '#')
    }
    // 继续监听
    setTimeout(listener, 100)
}
listener()

Process

  1. Create an iframe tag on the main page and specify the name;
  2. Create a new form tag, specify target as the name of the iframe just now, and add data and configuration requests;
  3. Submit the form and jump in the iframe;
  4. The server receives the request, generates and returns an HTML page according to the request parameters;
  5. iframe loads HTML and runs the script to modify the hash of the main page;
  6. The main page monitors the change of the hash, and clears the hash every time the hash value is obtained.

WindowHash

Error handling

  • The error event of the iframe is invalid in most browsers (default), so the error in the iframe is unknown to the main page;
  • Through the load event of the iframe, you can check whether the proxy page is loaded (the non-same source requires a hack method) to indirectly determine whether there is a network error, but the specific cause of the error is not known, that is, the server's response status code cannot be obtained ;
  • The remaining errors can be captured normally.

Practice tips

  • front end

    • The related attention points of form and iframe are the same as "HTML-P";
    • Set the main page hash should use window.location.replace , so that no history will be generated, which will affect the back operation of the main page;
    • Each hash setting requires a certain amount of cooling, and concurrency may be wrong;
    • It is not necessary to listen to the hashchange event for every request. You can set a unified event handler during initialization, use an object to save the callback of each request, assign a unique id, and call the callback by the id through the unified event handler;
    • If you follow the previous advice, the callback function in the global object needs to be cleaned up in time;
    • Since the iframe is a non-homogenous page (generated by the server), the main page url is not known, so the url needs to be passed to the server through parameters.
  • Server

The design idea of the front-end "unified event processor":

function initHashListener() {
  // 保存回调对象的对象
  const cbStore = {}
  // 设置监听,只需一个。
  window.addEventListener('hashchange', function () {
    // 处理 hash。
    ...
    try {
      // 运行失败分支。
      if (...) {
        cbStore[callbackId].reject(new Error(...))
        return
      }
      // 运行成功分支。
      cbStore[callbackId].resolve(...)
    } finally {
      // 执行清理。
      delete cbStore[callbackId]
    }
  })
  // 这里形成了一个闭包,只能用特定方法操作 cbStore。
  return {
    // 设置回调对象的方法。
    set: function (callbackId, resolve, reject) {
      // 回调对象包含成功和失败两个分支函数。
      cbStore[callbackId] = {
        resolve,
        reject
      }
    },
    // 删除回调对象的方法。
    del: function (callbackId) {
      delete cbStore[callbackId]
    }
  }
}
// 初始化,每次请求都调用其 set 方法设置回调对象。
const hashListener = initHashListener()

For specific code, please refer to the demo case WindowHash part source code.

to sum up

  • advantage

    • Any type of request can be sent (subject to browser form tag support);
    • No domain requirements, can be used for third-party APIs;
    • Supports ancient browsers (IE8-).
  • Disadvantage

    • Iframes have a great impact on browser performance, and multiple iframes are required for concurrency, which basically cannot be used in scenarios that require concurrency;
    • Concurrent scenes are prone to the problem of hash operation crashes. This problem is even more serious if the method of reading hashes in a loop is used to monitor. Unless there is a more rigorous anti-collision mechanism, concurrent use is strongly not recommended;
    • The amount of requested data is limited by the maximum length of the URL (different browsers vary);
    • Error capture is difficult, the specific cause of server error cannot be detected, and operation error cannot be captured;
    • Special interface support is required, and standard API specifications cannot be used.

In the next article, we will discuss the cross-origin of modern standards (HTML5), today's


calimanco
1.4k 声望766 粉丝

老朽对真理的追求从北爱尔兰到契丹无人不知无人不晓。