I. What is cross-domain?
1. What is the homology strategy and what are its limitations?
Same-origin policy is a kind of agreement, it is the most core and basic security features of the browser, if the lack of same-origin policy, the browser is very vulnerable to XSS, CSRF and other attacks. The so-called same-origin refers to the “protocol + domain name + port” are the same, even if two different domain names point to the same ip address, is not the same source.
The homology strategy restricts the content there:
- Cookie、LocalStorage、IndexedDB
- DOM nodes
- The AJAX request was sent and was intercepted by the browser.
But there are three tags that are allowed to load resources across domains:
<img src=XXX>
<link href=XXX>
<script src=XXX>
2. Common cross-domain scenarios
When any one of the protocol, subdomain, primary domain name and port number is not the same, it is considered as different domains. When different domains request resources from each other, it is considered “cross-domain”. Common cross-domain scenarios are shown in the figure below:
Two points in particular:
First: If the protocol and port caused by cross-domain problems “front office” can not help.
Second: In the cross-domain problem, only through the “URL first part” to identify and will not be based on the domain name corresponding to the IP address is the same to determine. The “first part of the URL” can be interpreted as “protocol, domain name and port must match”.
Here you may have a question: the request is cross-domain, so the request is sent or not?
Cross-domain is not that the request can’t be sent out, the request can be sent out, the server can receive the request and return the result normally, but the result is intercepted by the browser. You may wonder why Ajax doesn’t initiate a cross-domain request when you can do it through a form. Because ultimately, cross-domain is to prevent the user from reading the content of another domain, Ajax can get the response, the browser thinks it is not safe, so it intercepts the response. But the form doesn’t fetch new content, so it can initiate a cross-domain request. It also shows that cross-domain doesn’t completely stop CSRF, because the request is sent after all.
II. Cross-domain solutions
1.jsonp
1) JSONP Principle
By exploiting the vulnerability that the <script>
tag does not have cross-domain restrictions, a web page can get dynamically generated JSON data from other sources. JSONP requests must be supported by the other server.
2) Comparison of JSONP and AJAX
JSONP and AJAX are the same, are the client to send requests to the server side, from the server side to obtain data. However, AJAX belongs to the homology strategy, JSONP belongs to the non-homology strategy (cross-domain requests)
3) JSONP Advantages and Disadvantages
JSONP advantages are simple and good compatibility , can be used to solve the mainstream browser cross-domain data access problems . The disadvantage is that only support get method has limitations , not safe may be subject to XSS attacks.
4) JSONP implementation process
Declare a callback function with the name of the function (e.g., show) as the value of the parameter to be passed to the server requesting data across domains, and the function’s formal parameter as the target data to be fetched (the data returned by the server).
Create a<script>
tag, assign the address of the cross-domain API data interface to the script’s src, and pass the name of the function to the server in that address (you can pass the reference with a question mark:?callback=show).
After the server receives the request, it needs to do special processing: it takes the name of the function passed in and the data it needs to give you and splices it into a string, e.g., the name of the function passed in is show, and the data it’s ready to give you isshow('I don't love you.')
.
Finally, the server returns the prepared data to the client via the HTTP protocol, and the client then calls the callback function (show) that was declared before execution to manipulate the returned data.
In development, you may encounter multiple JSONP requests with the same callback function name, and then 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 code above is equivalent to requesting data from the address http://localhost:3000/say?wd=Iloveyou&callback=show
, then the backend returns show('I don't love you.')
and finally runs the show() function, which prints out ‘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 don't love you.')`)
})
app.listen(3000)
5) jQuery’s jsonp form
JSONP are GET and asynchronous requests, there is no other way of requesting and synchronous requests, and jQuery will clear the cache for JSONP requests by default.
$.ajax({
url:"http://crossdomain.com/jsonServerResponse",
dataType:"jsonp",
type:"get",
jsonpCallback:"show",
jsonp:"callback",
success:function (data){
console.log(data);}
});
2. cors
CORS requires both browser and backend support; IE 8 and 9 require XDomainRequest.
Browsers will automatically perform CORS communication, and the key to realize CORS communication is the backend. As long as the backend realizes CORS, it realizes cross-domain.
You can enable CORS by setting Access-Control-Allow-Origin on the server side. This attribute indicates which domains can access the resource, and if you set a wildcard it means that all websites can access the resource.
Although setting up CORS has nothing to do with the front-end, solving cross-domain problems in this way will result in two situations when sending requests, simple and complex requests.
1) Simple request
It is a simple request as long as both of the following two conditions are met
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
No event listeners are registered for any of the XMLHttpRequestUpload objects in the request; the XMLHttpRequestUpload object can be accessed using the XMLHttpRequest.upload property.
2) Complex requests
Requests that do not meet the above conditions are definitely complex requests. Complex request CORS request, will be in the formal communication before the addition of an HTTP query request, known as the “pre-check” request, the request is the option method, through which the request to know whether the server allows cross-domain requests.
When we use PUT
to make a request to the backend, it is a complex request and the backend needs to be configured as follows:
res.setHeader('Access-Control-Allow-Methods', 'PUT')
res.setHeader('Access-Control-Max-Age', 6)
if (req.method === 'OPTIONS') {
res.end()
}
app.put('/getData', function(req, res) {
console.log(req.headers)
res.end('I don't love you.')
})
Next we look at an example of a full complex request and introduce the fields associated with a CORS request
// index.html
let xhr = new XMLHttpRequest()
document.cookie = 'name=xiamen'
xhr.withCredentials = true
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)
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']
app.use(function(req, res, next) {
let origin = req.headers.origin
if (whitList.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin)
res.setHeader('Access-Control-Allow-Headers', 'name')
res.setHeader('Access-Control-Allow-Methods', 'PUT')
res.setHeader('Access-Control-Allow-Credentials', true)
res.setHeader('Access-Control-Max-Age', 6)
res.setHeader('Access-Control-Expose-Headers', 'name')
if (req.method === 'OPTIONS') {
res.end()
}
}
next()
})
app.put('/getData', function(req, res) {
console.log(req.headers)
res.setHeader('name', 'jw')
res.end('I don't love you.')
})
app.get('/getData', function(req, res) {
console.log(req.headers)
res.end('I don't love you.')
})
app.use(express.static(__dirname))
app.listen(4000)
The above code is a cross-domain request from http://localhost:3000/index.html
to http://localhost:4000/
. As we said above, the backend is the key to implementing CORS communication.
3. postMessage
postMessage is an API in HTML5 XMLHttpRequest Level 2 and one of the few window attributes that can be manipulated across domains:
- Data transfer between a page and its open new window
- Multi-Window Messaging
- Page and nested iframe messaging
- Cross-domain data transfer for the three scenarios above
The postMessage() method allows scripts from different sources to communicate in an asynchronous manner for limited purposes, enabling cross-text file, multi-window, and cross-domain messaging.
otherWindow.postMessage(message, targetOrigin, [transfer]);
message: Data to be sent to other windows.
targetOrigin: Specifies which windows can receive the message event via the window’s origin attribute, which can be either the string “*” (for unlimited) or a URI. when sending a message, if any of the three – protocol, host address, or port – of the target window does not match the value provided by targetOrigin, the message will not be sent; only if all three match exactly will the message be sent. sent; only if all three match exactly will the message be sent.
transfer(optional): a string of Transferable objects that are passed along with the message. Ownership of these objects will be transferred to the receiver of the message, and the sender will no longer retain ownership.
Let’s look at an example: http://localhost:3000/a.html
page passes “I love you” to http://localhost:4000/b.html
, which then passes back “I don’t love you”.
// a.html
<iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe>
//http://localhost:3000/a.html
<script>
function load() {
let frame = document.getElementById('frame')
frame.contentWindow.postMessage('I love you.', 'http://localhost:4000')
window.onmessage = function(e) {
console.log(e.data)
}
}
</script>
// b.html
window.onmessage = function(e) {
console.log(e.data)
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, as well as a cross-domain solution.WebSocket and HTTP are application layer protocols, both based on the TCP protocol. However, WebSocket is a two-way communication protocol, after the establishment of the connection, WebSocket server and client can actively send or receive data to each other. At the same time, WebSocket requires the HTTP protocol to establish a connection, and once the connection is established, the two-way communication between the client and server has nothing to do with HTTP.
The native WebSocket API is not very convenient to use, we use Socket.io
, which encapsulates the webSocket interface well, providing a simpler, flexible interface, but also provides backward compatibility with browsers that do not support webSocket.
Let’s look at an example: the local file socket.html sends and receives data to localhost:3000
.
// socket.html
<script>
let socket = new WebSocket('ws://localhost:3000');
socket.onopen = function () {
socket.send('I love you.');
}
socket.onmessage = function (e) {
console.log(e.data);
}
</script>
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');
let wss = new WebSocket.Server({port:3000});
wss.on('connection',function(ws) {
ws.on('message', function (data) {
console.log(data);
ws.send('I don't love you.')
});
})
5. Node middleware proxy (twice across domains)
Implementation Principle: The homology policy is a standard that the browser needs to follow, whereas if the server is requesting from the server it does not need to follow the homology policy. To proxy a server, you need to do the following steps:
- Accept client requests .
- Forwards the request to the server.
- Get server response data.
- Forward the response to the client.
Let’s take a look at an example: a local file, index.html, requests data from a target server, http://localhost:4000
, through a proxy server, http://localhost:3000
.
// 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: 'xiamen', 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>
const http = require('http')
const server = http.createServer((request, response) => {
response.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': 'Content-Type'
})
const proxyRequest = http
.request(
{
host: '127.0.0.1',
port: 4000,
url: '/',
method: request.method,
headers: request.headers
},
serverResponse => {
var body = ''
serverResponse.on('data', chunk => {
body += chunk
})
serverResponse.on('end', () => {
console.log('The data is ' + body)
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, and it is worth noting that the browser sends a request to the proxy server, which also follows the same-origin policy, and finally prints out in the index.html file {"title":"fontend","password":"123456"}
6. nginx reverse proxy
The implementation principle is similar to the Node middleware proxy, requiring you to build a transit nginx server for forwarding requests.
Using nginx reverse proxy to realize cross domain is the simplest way to cross domain. You only need to modify the configuration of nginx to solve the cross domain problem, support for all browsers, support for session, do not need to change any code, and will not affect the performance of the server.
Ideas for implementation: nginx configuration of a proxy server (domain name and domain1 the same, different ports) as a springboard machine, reverse proxy access to the domain2 interface, and you can modify the domain information in the cookie in the meantime, so that it is convenient for the current domain cookie writing, and realize the cross-domain login.
First, download nginx, and then modify nginx.conf in the nginx directory as follows.
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080;
proxy_cookie_domain www.domain2.com www.domain1.com;
index index.html index.htm;
add_header Access-Control-Allow-Origin http://www.domain1.com;
add_header Access-Control-Allow-Credentials true;
}
}
Finally, start nginx on the command line nginx -s reload
// index.html
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
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));
res.writeHead(200, {
'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly
});
res.write(JSON.stringify(params));
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
7.window.name + iframe
The window.name property is unique in that the name value persists after loading from a different page (or even from a different domain) and can support very long name values (up to 2MB).
Where a.html and b.html are in the same domain, both are http://localhost:3000
;and c.html is the 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, same domain as a.html, with empty content.
// c.html(http://localhost:4000/c.html)
<script>
window.name = 'I don't love you.'
</script>
Summarize: through the src attribute of the iframe from the external domain to the local domain, the cross-domain data is passed from the external 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
Realization principle: a.html wants to communicate with c.html across domains, through the intermediate page b.html to realize. Three pages, different domains use iframe’s location.hash to pass the value, and the same domain directly accesses js 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 into the hash value of a.html. Similarly, a.html and b.html are in the same domain, both are http://localhost:3000
;while 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 if the second level domain name is the same, for example, a.test.com
and b.test.com
are applicable to this method. Just add document.domain ='test.com'
to the page to indicate that the second level domain names are the same to realize the cross-domain.
Implementation principle: Both pages are forced to set document.domain as the base primary domain through js, it realizes the same domain.
Let’s look at an example: the page a.zf1.cn:3000/a.html
gets the value of a from the page b.zf1.cn:3000/b.html
// 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>
III. Summary
CORS supports all types of HTTP requests and is the fundamental solution for cross-domain HTTP requests
JSONP only supports GET requests. JSONP has the advantage of supporting older browsers and the ability to request data from sites that do not support CORS.
Whether it’s a Node middleware proxy or a nginx reverse proxy, the main idea is to leave the server unrestricted through a same-origin policy.
The cross-domain solutions used more often in daily work are cors and nginx reverse proxy