Nine cross-domain implementation principles (full version)
Front-end and back-end data interactions often encounter cross-domain requests. What is cross-domain, and what are the several cross-domain methods? This is the content of this article.
1. What is cross-domain?
1. What is the Same Origin Policy and what it restricts?
The same-origin policy is a convention. It is the core and most basic security function of the browser. If the same-origin policy is missing, the browser is vulnerable to XSS, CSRF and other attacks. The so-called homology means that "protocol + domain name + port" are the same, even if two different domain names point to the same IP address, they are not homologous. The same-origin policy restrictions include:
- Storage content such as Cookies, LocalStorage, IndexedDB
- DOM node
- After the AJAX request is sent, the result is intercepted by the browser
But there are three tags that allow cross-origin loading of resources:
- <img src=XXX>
- <link href=XXX>
- <script src=XXX>
2. Common cross-domain scenarios
When any of the protocol, subdomain name, main domain name, and port number are different, they are counted as different domains. Requesting resources from each other between different domains is considered "cross-domain". Common cross-domain scenarios are shown in the following figure:
Two points in particular:
First: If it is a cross-domain problem caused by protocols and ports, the "foreground" is powerless.
Second: In the cross-domain problem, it is only identified by the "header of the URL" and not based on whether the IP addresses corresponding to the domain name are the same. "The header of the URL" can be understood as "the protocol, domain name and port must match”.
Here you may have a question: the request is cross-domain, so is the request sent?
Cross-domain does not mean that the request cannot be sent, the request can be sent, the server can receive the request and return the result normally, but the result is intercepted by the browser. You may wonder if you can initiate cross-domain requests through forms, why not Ajax? Because in the final analysis, cross-domain is to prevent users from reading the content of another domain name, Ajax can get the response, the browser thinks this is not Safe, so the response is intercepted. But the form doesn't get new content, so cross-domain requests can be made. It also shows that cross-domain cannot completely prevent CSRF, because the request is sent after all.
2. Cross-domain solutions
1.jsonp
1) Principle of JSONP
By exploiting the vulnerability that <script>tags do not have cross-domain restrictions, web pages can obtain JSON data dynamically generated from other sources. JSONP requests must be supported by the other party's server.
2) JSONP and AJAX comparison
JSONP is the same as AJAX, both of which are the way that the client sends a request to the server and obtains data from the server. But AJAX belongs to the same-origin policy, JSONP belongs to the non-same-origin policy (cross-domain request)
3) Advantages and disadvantages of JSONP
The advantage of JSONP is that it is simple and compatible and can be used to solve the problem of cross-domain data access by mainstream browsers. The disadvantage is that only supporting the get method has limitations, and it is not safe and may suffer from XSS attacks.
4) Implementation process of JSONP
- Declare a callback function whose function name (such as show) is used as the parameter value to be passed to the server that requests data across domains, and the function parameter is the target data to be obtained (the data returned by the server).
- Create a <script>label, assign the address of the cross-domain API data interface to the src of the script, and pass the function name to the server in this address (you can pass the question mark parameter: ?callback=show).
- After the server receives the request, it needs to perform special processing: concatenate the function name passed in and the data it needs to give you into a string, for example: the function name passed in is show, and the data it has prepared is show('我不爱你').
- Finally, the server returns the prepared data to the client through the HTTP protocol, and the client then calls and executes the callback function (show) declared before to operate on the returned data.
In development, it may be encountered that the callback function names of multiple JSONP requests are the same. In this case, you need to encapsulate a JSONP function yourself.
// index.html function jsonp({ url, params, callback }) { return new Promise((resolve, reject) => { let script = document.createElement('script') window[callback] = function(data) { resolve(data) document.body.removeChild(script) } params = { ...params, callback } // wd=b&callback=show let arrs = [] for (let key in params) { arrs.push(`${key}=${params[key]}`) } script.src = `${url}?${arrs.join('&')}` document.body.appendChild(script) }) } jsonp({ url: 'http://localhost:3000/say', params: { wd: 'Iloveyou' }, callback: 'show' }).then(data => { console.log(data) })
The above code is equivalent to http://localhost:3000/say?wd=Iloveyou&callback=show
requesting data from this address, then returning to the background show('I do not love you')
, and finally running the show() function to print 'I don't love you'
// server.js let express = require('express') let app = express() app.get('/say', function(req, res) { let { wd, callback } = req.query console.log(wd) // Iloveyou console.log(callback) // show res.end(`${callback}('I do not love you')`) }) app.listen(3000)
5) jQuery's jsonp form
JSONP is all GET and asynchronous requests, there are no other request methods and synchronous requests, and jQuery will clear the cache for JSONP requests by default.
$.ajax({ url:"http://crossdomain.com/jsonServerResponse", dataType:"jsonp", type:"get",//can be omitted jsonpCallback:"show",//Customize the function name passed to the server, instead of using jQuery automatically generated, can be omitted jsonp:"callback",//->Put the parameter that passes the function name callback,can be omitted success:function (data){ console.log(data);} });
2.cors
CORS requires both browser and backend support. IE 8 and 9 need to do this via XDomainRequest .
The browser will automatically carry out CORS communication, and the key to realizing CORS communication is the backend. As long as the backend implements CORS, cross-domain is implemented.
The server can enable CORS by setting Access-Control-Allow-Origin. This attribute indicates which domain names can access the resource. If a wildcard is set, it means that all websites can access the resource.
Although setting CORS has nothing to do with the front end, if you solve the cross-domain problem in this way, there will be two situations when sending requests, namely simple requests and complex requests.
1) Simple request
As long as the following two conditions are met at the same time, it is a simple request
Condition 1: Use one of the following methods:
- GET
- HEAD
- POST
Condition 2: The value of Content-Type is limited to one of the following three:
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
None of the XMLHttpRequestUpload objects in the request have registered any event listeners; XMLHttpRequestUpload objects can be accessed using the XMLHttpRequest.upload property.
2) complex request
A request that does not meet the above conditions is definitely a complex request.
For CORS requests of complex requests, an HTTP query request will be added before the formal communication, which is called a "preflight" request. This request is an option method, and this request is used to know whether the server allows cross-domain requests.
When we use PUT
the request to the background, it is a complex request, and the background needs to be configured as follows:
// Which method is allowed to access me res.setHeader('Access-Control-Allow-Methods', 'PUT') res.setHeader('Access-Control-Max-Age', 6) // OPTIONS , The request does nothing if (req.method === 'OPTIONS') { res.end() } // Define the content returned by the background app.put('/getData', function(req, res) { console.log(req.headers) res.end('I don't love you') })
Next, let's look at an example of a complete complex request and introduce the fields related to the CORS request.
// index.html let xhr = new XMLHttpRequest() document.cookie = 'name=xiamen' // cookie cannot cross domain xhr.withCredentials = true // Whether the front-end settings have cookies xhr.open('PUT', 'http://localhost:4000/getData', true) xhr.setRequestHeader('name', 'xiamen') xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) { console.log(xhr.response) //Get the response header, set Access-Control-Expose-Headers in the background console.log(xhr.getResponseHeader('name')) } } } xhr.send()
//server1.js let express = require('express'); let app = express(); app.use(express.static(__dirname)); app.listen(3000);
//server2.js let express = require('express') let app = express() let whitList = ['http://localhost:3000'] //Set the whitelist app.use(function (req, res, next) { let origin = req.headers.origin if (whitList.includes(origin)) { // set which source can access me res.setHeader('Access-Control-Allow-Origin', origin) // which header is allowed to access me res.setHeader('Access-Control-Allow-Headers', 'name') // which method is allowed to access me res.setHeader('Access-Control-Allow-Methods', 'PUT') // allow to carry cookies res.setHeader('Access-Control-Allow-Credentials', true) // Preflight survival time res.setHeader('Access-Control-Max-Age', 6) // allow headers to be returned res.setHeader('Access-Control-Expose-Headers', 'name') if (req.method === 'OPTIONS') { res.end() // OPTIONS request does not do anything } } } next() ) app.put('/getData', function (req, res) { console.log(req.headers) res.setHeader('name', 'jw') //return a response header, which needs to be set in the background res.end('I dont love you') }) app.get('/getData', function (req, res) { console.log(req.headers) res.end('I dont love you') }) app.use(express.static(__dirname)) app.listen(4000)
The above code is made http://localhost:3000/index.htmlto http://localhost:4000/cross-domain request, as we said above, the backend is the key to realize CORS communication.
3.postMessage
postMessage is an API in HTML5 XMLHttpRequest Level 2, and is one of the few window attributes that can operate across domains. It can be used to solve the following problems:
- Data passing of the page and the new window it opens
- Message passing between multiple windows
- Pages with nested iframe messaging
- Cross-domain data transfer in the above three scenarios
The postMessage() method allows scripts from different sources to communicate asynchronously with limited communication, enabling cross-text document, multi-window, and cross-domain messaging .
otherWindow.postMessage(message, targetOrigin, [transfer]);
- message: The data to be sent to other windows.
- targetOrigin: Specify which windows can receive message events through the origin property of the window, and its value can be a string "*" (representing unlimited) or a URI. When sending a message, if any of the target window's protocol, host address or port does not match the value provided by targetOrigin, the message will not be sent; only if the three completely match, the message will be sent.
- transfer (optional): is a string of Transferable objects passed at the same time as the message. The ownership of these objects will be transferred to the receiver of the message, and the sender will no longer retain ownership.
Next let's look at an example: http://localhost:3000/a.html
the page http://localhost:4000/b.html
passes "I love you" to the page, and the latter returns "I don't love you".
// a.html <iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe> // wait for it to load to trigger an event //Embedded in http://localhost:3000/a.html <script> function load() { let frame = document.getElementById('frame') frame.contentWindow.postMessage('I love you', 'http://localhost:4000') //Send data window.onmessage = function(e) { //Accept return data console.log(e.data) //I don't love you } } </script>
// b.html window.onmessage = function(e) { console.log(e.data) //I love you e.source.postMessage('I don't love you', e.origin) } }
4.websocket
Websocket is a persistent protocol of HTML5, which realizes full-duplex communication between the browser and the server and is also a cross-domain solution. Both WebSocket and HTTP are application-layer protocols, both based on the TCP protocol. But WebSocket is a two-way communication protocol. After the connection is established, both the WebSocket server and client can actively send or receive data to each other. At the same time, WebSocket needs to use the HTTP protocol when establishing a connection. After the connection is established, the two-way communication between the client and the server has nothing to do with HTTP.
The native WebSocket API is not very convenient to use. We use Socket.io
it. It encapsulates the webSocket interface well, provides a simpler and more flexible interface, and provides backward compatibility for browsers that do not support webSocket.
Let's look at an example first: the local file socket.html sends localhost:3000
data to and receives data
// socket.html <script> let socket = new WebSocket('ws://localhost:3000'); socket.onopen = function () { Socket.send('I love you');//Send data to the server } socket.onmessage = function (e) { console.log(e.data);//Receive the data returned by the server } </script>
// server.js let express = require('express'); let app = express(); let WebSocket = require('ws');//Remember to install ws let wss = new WebSocket.Server({port:3000}); wss.on('connection',function(ws) { ws.on('message', function (data) { console.log(data); ws.send('I dont love you') }); })
5. Node middleware proxy (twice cross domain)
Implementation principle: The same-origin policy is a standard that browsers need to follow, and if it is a server-to-server request, it does not need to follow the same-origin policy.
A proxy server requires the following steps:
- Accept client requests.
- Forward the request to the server.
- Get the server response data.
- Forward the response to the client.
Let's look at an example first: the local file index.html file requests data http://localhost:3000
from the target server through the proxy server. http://localhost:4000
// index.html(http://127.0.0.1:5500) <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script> <script> $.ajax({ url: 'http://localhost:3000', type: 'post', data: { name: 'rendc', password: '123456' }, contentType: 'application/json;charset=utf-8', success: function(result) { console.log(result) // {"title":"fontend","password":"123456"} }, error: function(msg) { console.log(msg) } }) </script>
// server1.js proxy server (http://localhost:3000) const http = require('http') // Step 1: Accept the client request const server = http.createServer((request, response) => { // Proxy server, interacts directly with the browser, and needs to set the CORS header field response.writeHead(200, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': '*', 'Access-Control-Allow-Headers': 'Content-Type' }) // Step 2: forward the request to the server const proxyRequest = http .request( { host: '127.0.0.1', port: 4000, url: '/', method: request.method, headers: request.headers }, serverResponse => { // Step 3: Receive the response from the server var body = '' serverResponse.on('data', chunk => { body += chunk }) serverResponse.on('end', () => { console.log('The data is ' + body) // Step 4: Forward the response to the browser response.end(body) }) } ) .end() }) server.listen(3000, () => { console.log('The proxyServer is running at http://localhost:3000') })
// server2.js(http://localhost:4000) const http = require('http') const data = { title: 'fontend', password: '123456' } const server = http.createServer((request, response) => { if (request.url === '/') { response.end(JSON.stringify(data)) } }) server.listen(4000, () => { console.log('The server is running at http://localhost:4000') })
The above code goes through two cross-domains. It is worth noting that the browser sends a request to the proxy server, and also follows the same-origin policy, and finally prints it out in the index.html file{"title":"fontend","password":"123456"}
6. nginx reverse proxy
The implementation principle is similar to the Node middleware proxy, which requires you to build a transit nginx server to forward requests.
Using nginx reverse proxy to achieve cross-domain is the easiest way to cross-domain. You only need to modify the configuration of nginx to solve cross-domain problems, support all browsers, support sessions, do not need to modify any code, and will not affect server performance.
Implementation idea: Configure a proxy server (the domain name is the same as domain1, but the port is different) through nginx as a springboard, and the reverse proxy accesses the domain2 interface, and you can modify the domain information in the cookie by the way, which is convenient for writing the current domain cookie and realizes cross-domain login.
First download nginx , and then modify the nginx.conf in the nginx directory as follows:
// proxy server server { listen 80; server_name www.domain1.com; location / { proxy_pass http://www.domain2.com:8080; #reverse proxy proxy_cookie_domain www.domain2.com www.domain1.com; #Modify the domain name in the cookie index index.html index.htm; # When accessing nignx with a middleware proxy interface such as webpack-dev-server, there is no browser participation at this time, so there is no same-origin restriction, and the following cross-domain configuration may not be enabled add_header Access-Control-Allow-Origin http://www.domain1.com; #When the front end only cross-domain without cookies, it can be * add_header Access-Control-Allow-Credentials true; } }
nginx -s reloadFinally start nginx via command line
// index.html var xhr = new XMLHttpRequest(); // Front-end switch: whether the browser reads and writes cookies xhr.withCredentials = true; // Access the proxy server in nginx xhr.open('get', 'http://www.domain1.com:81/?user=admin', true); xhr.send(); // server.js var http = require('http'); var server = http.createServer(); var qs = require('querystring'); server.on('request', function (req, res) { var params = qs.parse(req.url.substring(2)); // write cookie to the foregroun res.writeHead(200, { 'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly: script cannot be read }); res.write(JSON.stringify(params)); res.end(); }); server.listen('8080'); console.log('Server is running at port 8080...');
7.window.name + iframe
The uniqueness of the window.name property: the name value persists after loading different pages (even different domain names) and can support very long name values (2MB).
Where a.html and b.html are of the same domain, both http://localhost:3000
; and c.html is http://localhost:4000
// a.html(http://localhost:3000/b.html) <iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe> <script> let first = true function load() { if(first){ let iframe = document.getElementById('iframe'); iframe.src = 'http://localhost:3000/b.html'; first = false; }else{ console.log(iframe.contentWindow.name); } } </script>
b.html is an intermediate proxy page, the same domain as a.html, and the content is empty.
// c.html(http://localhost:4000/c.html) <script> window.name = 'I do not love you' </script>
Summary: Through the src attribute of the iframe from the foreign domain to the local domain, the cross-domain data is passed from the foreign domain to the local domain by the window.name of the iframe. This cleverly bypasses the browser's cross-domain access restrictions, but at the same time it is a safe operation.
8.location.hash + iframe
Implementation principle: a.html wants to communicate with c.html across domains and realizes it through the intermediate page b.html. For three pages, the location.hash of iframe is used to pass values between different domains, and direct js access is used between the same domains to communicate.
Specific implementation steps: at first a.html passes a hash value to c.html, then c.html receives the hash value, then passes the hash value to b.html, and finally b.html puts the result in a.html in the hash value.
Similarly, a.html and b.html are of the same domain, both http://localhost:3000; and c.html is http://localhost:4000
// a.html <iframe src="http://localhost:4000/c.html#iloveyou"></iframe> <script> window.onhashchange = function () { console.log(location.hash); } </script>
// b.html <script> window.parent.parent.location.hash = location.hash </script>
// c.html console.log(location.hash); let iframe = document.createElement('iframe'); iframe.src = 'http://localhost:3000/b.html#idontloveyou'; document.body.appendChild(iframe);
9.document.domain + iframe
This method can only be used when the second-level domain names are the same, such as a.test.comand b.test.comare applicable to this method .
You only need to add to the page to document.domain ='test.com'indicate that the second-level domain name is the same to achieve cross-domain.
Implementation principle: Both pages are forced to set document.domain as the basic main domain through js, and the same domain is realized.
Let's look at an example: the page a.zf1.cn:3000/a.htmlgets b.zf1.cn:3000/b.htmlthe value of a in the page
// a.html <body> helloa <iframe src="http://b.zf1.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe> <script> document.domain = 'zf1.cn' function load() { console.log(frame.contentWindow.a); } </script> </body>
// b.html <body> hellob <script> document.domain = 'zf1.cn' var a = 100; </script> </body>
3. Summary
- CORS supports all types of HTTP requests and is the fundamental solution for cross-domain HTTP requests
- JSONP only supports GET requests. The advantage of JSONP is that it supports old browsers and can request data from websites that do not support CORS.
- Whether it is a Node middleware proxy or an nginx reverse proxy, the server is not restricted by the same-origin policy.
- In daily work, the most commonly used cross-domain solutions are cors and nginx reverse proxy