Understanding Cross-Domain Requests
Cross-domain refers to scenarios where resources from one domain are accessed by documents or scripts from another domain. This concept is broadly defined and includes several scenarios:
- Resource navigation: Links, redirects, form submissions
- Resource embedding: <link>, <script>, <img>, <frame> DOM elements, CSS files with background:url() or @font-face()
- Script requests: AJAX requests initiated by JavaScript, cross-domain operations on DOM and JavaScript objects
When developers discuss cross-domain issues, they typically refer to the restrictions imposed by the browser's same-origin policy.
The Same-Origin Policy
The Same-Origin Policy (SOP) is a security mechanism introduced by Netscape in 1995. It's a fundamental browser security feature that prevents malicious scripts from accessing data from another origin. Without this policy, browsers would be vulnerable to attacks like XSS (Cross-Site Scripting) and CSRF (Cross-Site Request Forgery).
Two origins are considered the same if they share the same protocol, domain, and port. Even if two different domains resolve to the same IP address, they are still considered different origins.
The same-origin policy restricts the following behaviors:
- Access to cookies, LocalStorage, and IndexDB
- Access to DOM elements and JavaScript objects
- Sending AJAX requests
Common Cross-Domain Scenarios
| URL | Description | Communication Allowed |
|---|---|---|
| http://www.example.com/a.js | Same domain, different files/paths | Allowed |
| http://www.example.com/b.js | ||
| http://www.example.com:8000/a.js | Same domain, different port | Not Allowed |
| http://www.example.com/b.js | ||
| http://www.example.com/a.js | Same domain, different protocol | Not Allowed |
| https://www.example.com/b.js | ||
| http://www.example.com/a.js | Domain and domain correspond to same IP | Not Allowed |
| http://192.168.1.100/b.js | ||
| http://www.example.com/a.js | Same main domain, different subdomains | Not Allowed |
| http://sub.example.com/b.js | ||
| http://example.com/c.js | ||
| http://www.site1.com/a.js | Different domains | Not Allowed |
| http://www.site2.com/b.js |
Cross-Domain Solutions
- JSONP (JSON with Padding)
- document.domain + iframe
- location.hash + iframe
- window.name + iframe
- postMessage API
- CORS (Cross-Origin Resource Sharing)
- Nginx Proxy
- Node.js Middleware Proxy
- WebSocket Protocol
1. JSONP Cross-Domain
JSONP leverages the fact that browsers allow loading scripts from different domains. By dynamically creating a script element and requesting a URL with a callback parameter, we can receive data from another domain.
Native JavaScript Implementation:
const script = document.createElement('script');
script.type = 'text/javascript';
// Pass a callback function name to the server for executing the response
script.src = 'http://api.example.com/login?user=admin&callback=handleResponse';
document.head.appendChild(script);
// Callback function to process the response
function handleResponse(data) {
alert(JSON.stringify(data));
}
jQuery AJAX Implementation:
$.ajax({
url: 'http://api.example.com/login',
type: 'get',
dataType: 'jsonp', // Request type is jsonp
jsonpCallback: "handleResponse", // Custom callback function name
data: {}
});
Vue.js Implementation:
this.$http.jsonp('http://api.example.com/login', {
params: {},
jsonp: 'handleResponse'
}).then((response) => {
console.log(response);
})
Node.js Server Example:
const querystring = require('querystring');
const http = require('http');
const server = http.createServer();
server.on('request', function(req, res) {
const params = querystring.parse(req.url.split('?')[1]);
const callbackName = params.callback;
// JSONP response setup
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.write(callbackName + '(' + JSON.stringify(params) + ')');
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
Limitation: JSONP only supports GET requests.
2. document.domain + iframe
This solution works only when the main domain is the same but subdomains differ. The principle is to set document.domain to the base domain in both pages.
Parent Window (http://www.example.com/page1.html):
<iframe id="frame" src="http://sub.example.com/page2.html"></iframe>
<script>
document.domain = 'example.com';
const userData = 'admin';
</script>
Child Window (http://sub.example.com/page2.html):
<script>
document.domain = 'example.com';
// Access parent window variables
alert('Data from parent: ' + window.parent.userData);
</script>
3. location.hash + iframe
This method uses a middle page to facilitate communication between two different domains. The pages use iframe's location.hash to pass values, and same-domain pages communicate directly through JavaScript.
Page A (http://domain1.com/a.html):
<iframe id="frame" src="http://domain2.com/b.html" style="display:none;"></iframe>
<script>
const frame = document.getElementById('frame');
// Send hash value to b.html
setTimeout(function() {
frame.src = frame.src + '#user=admin';
}, 1000);
// Callback function for same-domain c.html
function onCallback(response) {
alert('Data from c.html: ' + response);
}
</script>
Page B (http://domain2.com/b.html):
<iframe id="frame" src="http://domain1.com/c.html" style="display:none;"></iframe>
<script>
const frame = document.getElementById('frame');
// Listen for hash changes from a.html and forward to c.html
window.onhashchange = function () {
frame.src = frame.src + location.hash;
};
</script>
Page C (http://domain1.com/c.html):
<script>
// Listen for hash changes from b.html
window.onhashchange = function () {
// Call back to same-domain a.html with the result
window.parent.parent.onCallback('Response: ' + location.hash.replace('#user=', ''));
};
</script>
4. window.name + iframe
The window.name property maintains its value even when the page navigates to a different domain (or even after multiple page loads) and can store up to 2MB of data.
Page A (http://domain1.com/a.html):
const fetchData = function(url, callback) {
let state = 0;
const frame = document.createElement('iframe');
// Load cross-domain page
frame.src = url;
// onload triggers twice: first for cross-domain page, then for same-domain proxy
frame.onload = function() {
if (state === 1) {
// Second onload (same-domain proxy) - read window.name data
callback(frame.contentWindow.name);
cleanupFrame();
} else if (state === 0) {
// First onload (cross-domain) - switch to same-domain proxy
frame.contentWindow.location = 'http://domain1.com/proxy.html';
state = 1;
}
};
document.body.appendChild(frame);
// Clean up frame after data retrieval
function cleanupFrame() {
frame.contentWindow.document.write('');
frame.contentWindow.close();
document.body.removeChild(frame);
}
};
// Request data from cross-domain page B
fetchData('http://domain2.com/b.html', function(data){
alert(data);
});
Proxy Page (http://domain1.com/proxy.html):
This is a blank page in the same domain as a.html.
Page B (http://domain2.com/b.html):
<script>
window.name = 'This is data from domain2!';
</script>
5. postMessage API
The postMessage API is part of HTML5's XMLHttpRequest Level 2 and allows secure cross-domain communication between windows. It can be used for:
- Communication between a page and new windows it opens
- Communication between multiple windows
- Communication between a page and embedded iframes
- Cross-domain data transfer in the above scenarios
The syntax is: postMessage(data, origin)
- data: Any primitive type or cloneable object (best to serialize with JSON.stringify for broader browser support)
- origin: Protocol + hostname + port. Can be "*" for any window, or "/" for same-origin only
Page A (http://domain1.com/a.html):
<iframe id="frame" src="http://domain2.com/b.html" style="display:none;"></iframe>
<script>
const frame = document.getElementById('frame');
frame.onload = function() {
const data = {
name: 'aym'
};
// Send cross-domain data to domain2
frame.contentWindow.postMessage(JSON.stringify(data), 'http://domain2.com');
};
// Receive response from domain2
window.addEventListener('message', function(event) {
alert('Data from domain2: ' + event.data);
}, false);
</script>
Page B (http://domain2.com/b.html):
<script>
// Receive data from domain1
window.addEventListener('message', function(event) {
alert('Data from domain1: ' + event.data);
const data = JSON.parse(event.data);
if (data) {
data.value = 16;
// Send processed data back to domain1
window.parent.postMessage(JSON.stringify(data), 'http://domain1.com');
}
}, false);
</script>
6. Cross-Origin Resource Sharing (CORS)
CORS is a W3C standard that allows servers to indicate which origins are permitted to access their resources. For simple cross-domain requests, only the server needs to set the Access-Control-Allow-Origin header. For requests with cookies, both client and server need additional configuration.
Important: Due to the same-origin policy, cookies read will be from the target domain (not the current page). To write cookies to the current domain, see the nginx and Node.js proxy solutions below.
All modern browsers support CORS (IE8+ requires XDomainRequest for support).
Client-Side Configuration:
Native AJAX:
// Enable cookies in cross-domain requests
const xhr = new XMLHttpRequest(); // For IE8/9, use window.XDomainRequest
xhr.withCredentials = true;
xhr.open('post', 'http://api.example.com/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText);
}
};
jQuery AJAX:
$.ajax({
...
xhrFields: {
withCredentials: true // Enable cookies
},
crossDomain: true, // Include cross-domain headers (but not cookies)
...
});
Vue.js Frameworks:
With axios:
axios.defaults.withCredentials = true
With vue-resource:
Vue.http.options.credentials = true
Server-Side Configuration:
Java Backend:
/*
* Import: import javax.servlet.http.HttpServletResponse;
* Add parameter: HttpServletResponse response
*/
// Allowed domain for cross-origin access (include protocol+domain+port if applicable)
response.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com");
// Enable cookies in requests - note: this requires a specific domain, not "*"
response.setHeader("Access-Control-Allow-Credentials", "true");
// Custom headers for OPTIONS preflight requests
response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");
Node.js Backend Example:
const http = require('http');
const server = http.createServer();
const qs = require('querystring');
server.on('request', function(req, res) {
let postData = '';
// Collect POST data
req.addListener('data', function(chunk) {
postData += chunk;
});
// Process complete request
req.addListener('end', function() {
postData = qs.parse(postData);
// CORS configuration
res.writeHead(200, {
'Access-Control-Allow-Credentials': 'true', // Allow cookies
'Access-Control-Allow-Origin': 'http://www.domain1.com', // Specific allowed domain
'Set-Cookie': 'sessionId=abc123;Path=/;Domain=www.example.com;HttpOnly' // HttpOnly prevents JS access
});
res.write(JSON.stringify(postData));
res.end();
});
});
server.listen('8080');
console.log('Server is running at port 8080...');
7. Nginx Proxy for Cross-Domain
Solving IconFont Cross-Domain Issues:
Browsers typically allow cross-domain access to static resources like JS, CSS, and images, but font files (eot|otf|ttf|woff|svg) are an exception. This can be resolved with the following nginx configuration:
location / {
add_header Access-Control-Allow-Origin *;
}
Nginx Reverse Proxy for API Cross-Domain:
Principal: The same-origin policy is a browser security feature, not part of HTTP protocol. Server-side HTTP calls don't execute JavaScript and aren't subject to same-origin restrictions.
Approach: Configure nginx as a reverse proxy server with the same domain as the client but a different port. This server can forward requests to the target API and modify cookie domains to enable cookie-based authentication.
Nginx Configuration:
# Proxy server
server {
listen 81;
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 cookie domain
index index.html index.htm;
# When using webpack-dev-server or similar middleware, this may not be needed
add_header Access-Control-Allow-Origin http://www.domain1.com;
add_header Access-Control-Allow-Credentials true;
}
}
Frontend Code Example:
const xhr = new XMLHttpRequest();
// Enable cookies in cross-domain requests
xhr.withCredentials = true;
// Access the nginx proxy server
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();
Node.js Backend Example:
const http = require('http');
const server = http.createServer();
const qs = require('querystring');
server.on('request', function(req, res) {
const params = qs.parse(req.url.substring(2));
// Set cookie for the client
res.writeHead(200, {
'Set-Cookie': 'sessionId=abc123;Path=/;Domain=www.domain2.com;HttpOnly'
});
res.write(JSON.stringify(params));
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
8. Node.js Middleware Proxy for Cross-Domain
This approach uses a Node.js middleware server as a proxy to forward requests, similar to nginx. It can also modify cookie domains to enable cookie-based authentication across domains.
Non-Vue Framework Cross-Domain (Two Cross-Domain Requests):
Using Node.js + Express + http-proxy-middleware to create a proxy server.
Frontend Code Example:
const xhr = new XMLHttpRequest();
// Enable cookies in cross-domain requests
xhr.withCredentials = true;
// Access the http-proxy-middleware proxy server
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();
Middleware Server:
const express = require('express');
const proxy = require('http-proxy-middleware');
const app = express();
app.use('/', proxy({
// Target API for proxying
target: 'http://www.domain2.com:8080',
changeOrigin: true,
// Modify response headers for CORS and cookies
onProxyRes: function(proxyRes, req, res) {
res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
res.header('Access-Control-Allow-Credentials', 'true');
},
// Modify cookie domain
cookieDomainRewrite: 'www.domain1.com' // Can be false to skip modification
}));
app.listen(3000);
console.log('Proxy server is running at port 3000...');
Vue Framework Cross-Domain (Single Cross-Domain Request):
Using Node.js + webpack + webpack-dev-server for API proxying. In development mode, the Vue rendering server and API proxy run on the same webpack-dev-server, eliminating the need for CORS headers between the page and proxy.
webpack.config.js Configuration:
module.exports = {
entry: {},
module: {},
...
devServer: {
historyApiFallback: true,
proxy: [{
context: '/login',
target: 'http://www.domain2.com:8080', // Target API
changeOrigin: true,
secure: false, // Use for HTTPS APIs that cause errors
cookieDomainRewrite: 'www.domain1.com' // Can be false to skip
}],
noInfo: true
}
}
9. WebSocket Protocol for Cross-Domain
WebSocket is a HTML5 protocol that enables full-duplex communication between browsers and servers, with built-in support for cross-domain communication. It's an excellent implementation of server-push technology.
While the native WebSocket API works, libraries like Socket.io provide more convenient interfaces and backward compatibility for browsers that don't support WebSocket.
Frontend Code:
<div>User input: <input type="text"></div>
<script src="https://cdn.jsdelivr.net/npm/socket.io-client@2.3.0/dist/socket.io.js"></script>
<script>
const socket = io('http://www.domain2.com:8080');
// Connection established
socket.on('connect', function() {
// Listen for server messages
socket.on('message', function(msg) {
console.log('Data from server: ' + msg);
});
// Listen for server disconnection
socket.on('disconnect', function() {
console.log('Server connection closed.');
});
});
document.querySelector('input').onblur = function() {
socket.send(this.value);
};
</script>
Node.js WebSocket Server:
const http = require('http');
const socketIo = require('socket.io');
// Create HTTP server
const 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...');
// Set up WebSocket server
socketIo(server).on('connection', function(client) {
// Handle incoming messages
client.on('message', function(msg) {
client.send('Hello: ' + msg);
console.log('Data from client: ' + msg);
});
// Handle disconnection
client.on('disconnect', function() {
console.log('Client disconnected.');
});
});