Cross – domain is a common problem in front – end development. In our daily work, we use interface proxies like Webpack-dev-server to build our development environment, or use interface proxies like Charles. It can be solved by operation and maintenance with NGINX or CORS and other solutions.

What is cross-domain?

In JavaScript, there is an important security restriction called the “same-Origin Policy.” This policy places an important restriction on the page content that JavaScript code can access, namely that JavaScript can only access content that is in the same domain as the document that contains it. Cross-domain means that the browser cannot execute scripts from other sites. Explanation on MDN (The browser’s same-origin policy restricts how documents or scripts loaded from the same source can interact with data from another source, an important mechanism for isolating potentially malicious files). In short, this is the security mechanism that browsers implement on scripts.

Definition of homology

If two pages have the same protocol, port (if specified), and host, the two pages have the same source. If one of the protocols, ports, or hosts is different, they are not the same source.

Url call Url The results of
www.peanutyu.site/home www.peanutyu.site/api/* Call successful, non-cross-domain
www.peanutyu.site/home www.peanut.site/api/* The call failed. The primary domain name is different
www.peanutyu.site/home www.peanutyu.site/api/* Call failed, different protocol
www.peanutyu.site/home blog.peanutyu.site/api/* Call failed. Subdomain name is different
www.peanutyu.site/home www.peanutyu.site:8080/api/* Call failed, port number is different

JSONP Cross-domain (JSON with padding)

In HTML tags, some tags such as script, IMG, iframe and other tags to obtain resources have no cross-domain restrictions. JSONP is to dynamically create a script tag and request a reference url to achieve cross-domain communication. Because script tags load resources as GET requests, JSONP can only send GET requests.

Background interface design

const xxService = require('.. /.. /service/xxService');
exports = module.exports = new class {
  constructor() {}
  
  jsonp () {
    let [cb, username ] = [];
    if (ctx.query) {
      ({ cb, username } = ctx.query);
    }
    const data = await xxService.xxMethods(username);
    // the cb argument is the method name of the convention. The back end returns a directly executed method to the front end. The front end takes the method and executes it immediately, putting the returned data into the method parameters.
    ctx.body = `${cb}(The ${JSON.stringify(data)}) `; }}Copy the code

Front-end method implementation

Native implementation

  const script = document.createElement('script');
  const body = document.body;
  script.src = 'http://127.0.0.1:3000/api/jsonp? cb=callbackJsonp&username=peanut';
  body.appendChild(script);

  function callbackJsonp(res) {
    const div = document.createElement('div');
    div.innerText = JSON.stringify(res);
    body.appendChild(div);
    body.removeChild(script);
  }
Copy the code

Jquery implementation

  $.ajax({
    url: 'http://blog.peanutyu.site/api/*'.type: 'GET'.dateType: 'jsonp'.// Set the request mode to JSONp
    jsonpCallback: 'callbackJsonp'.data: {
      'username': 'peanut',}});function callbackJsonp(res) {
    console.log(res);
  }
Copy the code

The iframe cross-domain

Document.domain + iframe cross domain

This cross-domain approach requires the same primary domain name. For example, the primary domain name www.peanut.site, blog.peanut. Site, and A.peanutyu. site are all peanutyu.site. This cross-domain mode cannot be used if the main domain name is different.

Browser pages in different domains cannot interact with EACH other through JS. Different pages, however, can get each other’s Window objects. However, we only get a window object that is almost useless. Such as its address for http://www.peanutyu.site/a.html, a page on this page have a iframe, its SRC for http://peanutyu.site/b.html, the page and its internal iframe is different domains, so we are There is no way to get something in an iframe by writing JS code in the page. We only need the http://www.peanutyu.site/a.html and http://peanutyu.site/b.html are set to the same domain name.

It is important to note that the document.domain Settings are limited. We can only set the document.domain as the parent of itself or a higher level, and the primary domain must be the same. A document in blog.peanutyu.site can be set to document.domain as blog.peanutyu.site or any of the peanutyu.site, but not a.peanutyu. site. This is the subdomain of the current domain, and cannot be set to baidu.com because the primary domain is different.

Suppose we want to be on the http://www.peanutyu.site/a.html page on http://peanutyu.site

In setting document. http://www.peanutyu.site/a.html domain


      
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>A page</title>
</head>
<body>
  <iframe id="iframe" src="http://peanutyu.site/b.html" style="display:none;"></iframe>
  <script>
    $(function () {
      try {
        document.domain = "peanutyu.site"; // Here document.domain is set the same
      } catch (e) { }
      $("#iframe").load(function () {
        var iframe = $("#iframe").contentDocument.$;
        iframe.get("http://peanutyu.site/api".function (data) {
          console.log(data);
        });
      });
    });
  </script>
</body>
</html>
Copy the code

Also need to set up the document in http://peanutyu.site/b.html. Domain.


      
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>B page</title>
</head>
<body>
  <script>
    $(function () {
      try {
        document.domain = "peanutyu.site"; // Here document.domain is set the same
      } catch (e) { }
    });
  </script>
</body>
</html>
Copy the code

It should be noted that page A can be obtained from page B only after page B is loadedObject we can send Ajax requests directly, but this cross-domain approach can only be used if the primary domain is the same.

Window. name + iframe cross domain

When the iframe page jumps to another address, its window.name value remains the same and supports storing very long names (2MB). However, browsers do not allow cross-domain iframes to call each other or pass values. Window. name does not change when the iframe is called. We can use this feature to pass values to each other. Of course, it is not allowed to read the value of the window.name of the iframe across domains.

If the SRC source of the index.html page and the SRC source of the iframe in the page are different, you cannot manipulate any content inside the iframe. Therefore, you cannot obtain the window.name attribute of the iframe. But since homologous, we can prepare a and the main page http://www.peanut.com/a.html agent under the same domain page http://www.peanut.com/proxy.html to specify the SRC.

Suppose we have a need to get to the data from the http://peanut.site/data.html page http://peanutyu.site/a.html

Data page code

window.name = 'I'm data on the data page';
Copy the code

A Page code

const iframe = document.createElement('iframe');
iframe.style.display = 'none';
let state = 0;

iframe.onload = function() {
  if (state === 1) {
    const data = iframe.contentWindow.name;
    iframe.contentWindow.document.write(' ');
    iframe.contentWindow.close();
    document.body.removeChild(iframe);
  } else {
    state = 1;
    iframe.contentWindow.location = 'http://peanutyu.site/proxy.html';
  }
}
iframe.src = 'http://peanut.site/data.html';
document.body.appendChild(iframe);
Copy the code

In the process of loading an iframe, a quick reset of the iframe’s location is equivalent to reloading the page, which calls the onload method of the iframe again. In this case, we go inside the condition state === 1 and get the value of the iframe window.name. Since window.name does not change when iframe is called, we get the value of window.name in different fields.

Cross-domain resource sharing CORS

Introduction to the

CORS requires both browser and server support. Currently, all browsers support this function, and Internet Explorer cannot be lower than Internet Explorer 10. The entire CORS communication process is completed automatically by the browser without user participation. For developers, CORS communication is no different from same-origin AJAX communication, and the code is exactly the same. As soon as the browser discovers that an AJAX request crosses the source, it automatically adds some additional headers, and sometimes an additional request, but the user doesn’t feel it. Therefore, the key to CORS communication is the server. As long as the server implements the CORS interface, cross-source communication is possible.

Two kinds of requests

Browsers classify CORS requests into two categories: Simple request and not-so-simple Request. As long as the following two conditions are met, it is a simple request. (1) The request method is one of three methods:

  • HEAD
  • GET
  • POST

(2) HTTP headers do not exceed the following fields:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-type: Application/X-www-form-urlencoded, multipart/form-data, text/plain

Any request that does not meet both conditions is a non-simple request. Browsers treat these two requests differently.

A simple request

The basic flow

For simple requests, the browser issues CORS requests directly. Specifically, add an Origin field to the header information. As an example, the browser automatically adds an Origin field to the header when it realizes that the cross-source AJAX request is a simple one.

GET /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com Accept-language: en-us Connection: Keep alive - the user-agent: Mozilla / 5.0...Copy the code

In the header above, the Origin field specifies the source (protocol + domain + port) from which the request came. Based on this value, the server decides whether to approve the request or not.

If Origin does not specify a licensed source, the server will return a normal HTTP response. The browser realizes that the response header does not contain the Access-Control-Allow-Origin field (more on that below), and it throws an error that is caught by XMLHttpRequest’s onError callback. Note that this error cannot be identified by the status code, because the status code for the HTTP response might be 200.

If Origin specifies a domain name within the license, the server returns a response with several additional header fields.

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
Copy the code

In the header above, there are three fields related to CORS requests, all beginning with Access-Control-.

Access-Control-Allow-Origin

This field is required. Its value is either the value of the Origin field at the time of the request, or an *, indicating acceptance of requests for any domain name.

Access-Control-Allow-Credentials

This field is optional. Its value is a Boolean value indicating whether cookies are allowed to be sent. By default, cookies are not included in CORS requests. If set to true, the server explicitly approves that cookies can be included in the request and sent to the server. This value can only be set to true if the server does not want the browser to send cookies.

Access-Control-Expose-Headers

This field is optional. In CORS requests, the getResponseHeader() method of the XMLHttpRequest object takes only six basic fields: Cache-control, Content-language, Content-Type, Expires, Last-Modified, Pragma. If you want to get other fields, you must specify access-Control-expose-headers. The above example specifies that getResponseHeader(‘FooBar’) can return the value of the FooBar field.

WithCredentials attribute

As mentioned above, CORS requests do not send cookies and HTTP authentication information by default. To send cookies to the server, specify the access-Control-allow-credentials field with the server’s permission.

Access-Control-Allow-Credentials: true
Copy the code

On the other hand, the developer must turn on the withCredentials attribute in the AJAX request.

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
Copy the code

Otherwise, the browser won’t send a Cookie, even if the server agrees to do so. Or, if the server asks for a Cookie, the browser won’t handle it. However, if the withCredentials setting is omitted, some browsers still send cookies together. In this case, you can explicitly disable the withCredentials.

xhr.withCredentials = false;
Copy the code

Note that access-Control-allow-Origin cannot be set to an asterisk if cookies are to be sent, and must specify an explicit domain name consistent with the requested web page. At the same time, cookies still follow the same origin policy, only the Cookie set with the server domain name will be uploaded, cookies of other domain names will not be uploaded, and (cross-source) document. Cookie in the original web page code can not read cookies under the server domain name.

Non-simple request

Preview the request

Non-simple requests are requests that have special requirements on the server, such as the request method being PUT or DELETE, or the content-Type field being of Type Application/JSON. CORS requests that are not simple requests are preceded by an HTTP query request, called a “preflight” request. The browser asks the server if the domain name of the current web page is on the server’s license list, and what HTTP verb and header fields can be used. The browser issues a formal XMLHttpRequest request only if it receives a positive response; otherwise, an error is reported. The following is a JavaScript script from the browser.

var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header'.'value');
xhr.send();
Copy the code

In the code above, the HTTP request is PUT and sends a Custom Header x-custom-header. The browser realizes that this is not a simple request and automatically issues a “pre-check” request, asking the server to confirm that it is ok to do so. Here is the HTTP header for this “precheck” request.

OPTIONS /cors HTTP/1.1
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...
Copy the code

The request method for the “precheck” request is OPTIONS, indicating that the request is being queried. In the header information, the key field is Origin, indicating which source the request came from. In addition to the Origin field, the precheck request header contains two special fields.

Access-Control-Request-Method

This field is required to list which HTTP methods are used by the browser for CORS requests, in this example PUT.

Access-Control-Request-Headers

This field is a comma-separated string that specifies the additional Header field to be sent by a browser CORS request, x-custom-header in the example above.

Response to precheck request

After receiving the precheck Request, the server checks the Origin, access-Control-request-method, and access-Control-request-headers fields and confirms that cross-source requests are allowed, it can respond.

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://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
Copy the code

In the HTTP response above, the key is the Access-Control-Allow-Origin field, which indicates that http://api.bob.com can request data. This field can also be set to an asterisk to indicate approval of any cross-source request.

Access-Control-Allow-Origin: *
Copy the code

If the browser denies the “precheck” request, it will return a normal HTTP response, but without any CORS related header fields. At this point, the browser decides that the server did not approve the precheck request, and therefore fires an error that is caught by the ONError callback function of the XMLHttpRequest object. The console will print the following error message.

XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
Copy the code

Other CORS related fields that the server responds to are as follows.

Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
Copy the code
Access-Control-Allow-Methods

This field is required, and its value is a comma-separated string indicating all methods supported by the server for cross-domain requests. Notice that all supported methods are returned, not just the one requested by the browser. This is to avoid multiple “pre-check” requests.

Access-Control-Allow-Headers

The access-Control-allow-HEADERS field is required if the browser Request includes the access-Control-request-HEADERS field. It is also a comma-separated string indicating all header information fields supported by the server, not limited to those requested by the browser in precheck.

Access-Control-Allow-Credentials

This field has the same meaning as a simple request.

Access-Control-Max-Age

This field is optional and specifies the validity period of the precheck request, in seconds. In the result above, the validity period is 20 days (1728000 seconds), which allows the response to be cached for 1728000 seconds (20 days), during which time another precheck request is not issued.

Normal browser requests and responses

Once the server passes the “precheck” request, every subsequent normal BROWSER CORS request will have the same Origin header field as a simple request. The server also responds with an Access-Control-Allow-Origin header field.

The following is a normal CORS request for the browser after the “precheck” request.

PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
Copy the code

The Origin field in the header above is automatically added by the browser. The following is a normal response from the server.

Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8
Copy the code

In the header above, the access-Control-Allow-Origin field is mandatory for each response.

Comparison with JSONP

CORS serves the same purpose as JSONP, but is more powerful than JSONP. JSONP supports only GET requests, and CORS supports all types of HTTP requests. JSONP has the advantage of supporting older browsers and being able to request data from sites that do not support CORS.

The WebSocket protocol is cross-domain

WebSocket is a protocol for full duplex communication over a single TCP connection provided by HTML5.

WebSocket makes it easier to exchange data between the client and the server, allowing the server to actively push data to the client. In the WebSocket API, the browser and server only need to complete a handshake to create a persistent connection and two-way data transfer.

The native WebSocket API is not very convenient to use. We use socket. IO, which encapsulates the WebSocket interface well, provides a simpler, flexible interface, and provides backward compatibility for browsers that do not support WebSocket.

The front-end code

<div>
  <input type="text" id="inputText">
</div>
<script src="example.com/socket.io.js"></script>
<script>
var socket = io('http://www.peanutyu.site');

// The connection was successfully processed
socket.on('connect'.function() {
    // Listen for server messages
    socket.on('message'.function(msg) {
        console.log('data from server: ---> ' + msg); 
    });

    // The listener server is closed
    socket.on('disconnect'.function() { 
        console.log('Server socket has closed.'); 
    });
});

document.getElementById('inputText').onblur = function() {
    socket.send(this.value);
};
</script>
Copy the code

Node Server

var http = require('http');
var socket = require('socket.io');

// Start the HTTP service
var server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-type': 'text/html'
    });
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080... ');

// Listen for socket connections
socket.listen(server).on('connection'.function(client) {
    // Receive information
    client.on('message'.function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
    });

    // Disconnect processing
    client.on('disconnect'.function() {
        console.log('Client socket has closed.'); 
    });
});
Copy the code

HTML5 postMessage

HTML5 Window.postMessage is a secure, event-based messaging API.

Call the postMessage method in the source window where the message needs to be sent to send messages out. The source window can be in the following situations.

  1. Global window object (var win = window)
  2. Iframe in documentWindow (var win = iframe.documentwindow)
  3. Parent of the current document window (var win = window.parent)
  4. New window opened by JavaScript (var win = window.open())

Send a message

win.postMessage(msg, targetOrigin);
Copy the code

PostMessage accepts two arguments

  1. MSG, the message that needs to be sent, can be any JavaScript parameter; Such as strings, numbers, objects, arrays, etc.
  2. TargetOrigin, which is the target field to which the message is to be sent, is http://www.peanut.site/ if the web page at www.peanutyu.site needs to send the message to the web page at www.peanut.site. If any of the protocol, host address, or port of the target window does not match the value provided by targetOrigin, the message will not be sent. A message will only be sent if all three match. The value can also be passed as a string ‘*’ for unrestricted.)

Receives the message

window.addEventListener('message'.function receiveMessage(event) {
  if (event.origin === 'http://www.peanut.site') {
    console.log(event.data); // The data passed}},false);
Copy the code

The properties of the event are

  1. Data An object passed from another window.
  2. Origin The origin of the message sender window when calling postMessage. The string is a combination of protocol, ://, domain name, and: port number. For example, example.org (implied port 443), example.net (implied port 80), example.com:8080. Note that this Origin is not guaranteed to be the current or future origin of the window, as postMessage may be navigated to a different location after being called.
  3. Source A reference to the window object from which the message was sent; You can use this to establish two-way communication between two Windows with different origins.

Nginx proxy

Nginx configuration

server{
    Listen on port 9999
    listen 9999;
    Localhost = localhost
    server_name localhost;
    If localhost:9999/ API is used, send it to http://localhost:9871location ^~ /api { proxy_pass http://localhost:9871; }}Copy the code

Nginx will listen for any localhost:9099/ API address and forward it to the real server address http://localhost:9871

axios.get('http://localhost:9999/api/iframePost', params).then(result= > console.log(result)).catch((a)= > {});
Copy the code

The way Nginx forwards seems convenient! If the back-end interface is a public API, such as some public service to get the weather, the front end of the call should not be used to configure Nginx, if compatibility is ok (IE 10 or above), CROS is more general practice.

Node mid-layer interface forwarding

Currently, the interface processing mode is commonly used in the background. Spa pages are routed by the front end via server root routing or /index rendering. The rest of the/API path to develop our interface request.

The background configuration

// Page routing
router.get('/index'.async function(ctx, next) {
  // Package the JS timestamp
  let timeT = moment().valueOf();
  // Set the base version number
  let buildPath = config.assetsServerName;
  try {
    let env = process.env.NODE_ENV;
    // Get the JS version from Redis
    let configInfo = await RedisService.getServerConfigInfoByEnv(env);
    if(configInfo) {
      let info = JSON.parse(configInfo);
      if(info && info['build']) {
        buildPath = info['build'].url; }}// Render SPA page
    await ctx.render('index', {assetsPath: buildPath, tag: timeT});
  } catch(e) {
    // An error was reported in the render configuration version SPA page
    await ctx.render('index', {assetsPath: buildPath, tag: timeT}); }})// Interface routing
router.get('/api/list', xxController.methods); // Get the list method, specific logic processing is done by controller
Copy the code

The front-end code

_reqData() {
  axios.get('/api/list', {}).then(result= > {console.log(result)}).catch((a)= > {});
}

componentWillMount() {
  this._reqData();
}
Copy the code

Refer to the link

  • Cross-domain resource sharing (CORS