Recently, I encountered a special cross-domain issue while working on front-end and back-end interactions. I have dealt with cross-domain issues to some extent in my projects, but this situation was unique. I took this opportunity to summarize the solutions to cross-domain problems for future reference.
Scenario#
Some time ago, I was learning front-end development, so I wanted to take this opportunity to create a small tool for fun. Thus, I ended up with this extremely rudimentary bullet screen website.
To achieve persistent storage of the bullet screen, I attempted to implement front-end and back-end interaction to store the bullet screen in a database, and then the front end would pull data from the back end at regular intervals.
The implementation was relatively simple, but when I started testing sending requests to the back end locally, the problem arose:
It was clearly a cross-domain issue, so next we needed to apply what we had learned to solve it.
First step, analyze the cause of the problem: What is cross-domain?
What is Cross-Domain?#
Cross-domain refers to a document or script under one domain attempting to request resources from another domain.
Cross-domain is not a bug; rather, it is due to a series of "same-origin policies" specified by browsers for security reasons. These same-origin policies ensure the safety of user information and prevent data from being stolen by malicious websites.
This policy restricts certain behaviors of web pages to be conducted only within web pages that are "same-origin" with themselves. If they are not same-origin, it will result in the failure of that behavior, which is cross-domain failure.
Here we have two concepts: one is same-origin, and the other is cross-domain behavior.
The definition of same-origin includes three aspects:
- Same protocol
- Same domain name
- Same port
Only when all three conditions are met can two web pages be considered "same-origin."
Cross-domain behavior (a term I defined, roughly referring to operations affected by the same-origin policy) has become increasingly broad with the development of the internet. Common cross-domain behaviors include:
- Accessing Cookie, LocalStorage, and IndexDB
- Accessing DOM and JS objects
- AJAX requests
We can see that the most common AJAX requests in front-end and back-end interactions are also listed here, making it inevitable to solve cross-domain issues in front-end and back-end interactions.
So why define the same-origin policy? Wouldn't it be better without cross-domain restrictions?
Without cross-domain restrictions, web pages would be easily susceptible to attacks such as XSS and CSRF. Because there are no restrictions, malicious websites could also freely launch attacks, significantly increasing the maintenance costs of websites. Therefore, the same-origin policy is actually a double-edged sword; while it protects web pages, it occasionally misfires against friendly forces.
Now that we understand the root of the problem, we can find targeted solutions.
Solutions#
We can see that the key to the cross-domain issue lies in our Mrequest being within the restricted scope and not achieving same-origin, which leads to the problem.
The key points have been highlighted, and our solutions can be approached from these two ideas:
- Adopt behaviors that are not restricted by the same-origin policy.
- Find ways to make the behavior same-origin.
Here we focus on AJAX requests. For other cross-domain solutions such as cookies and iframes, referring to relevant blogs should yield answers.
1. JSONP Cross-Domain#
We can achieve cross-domain by defining the JSONP type in AJAX requests. However, JSONP essentially uses a completely different request method than AJAX.
Traditional AJAX requests are actually asynchronous requests using xhr
, while JSONP essentially constructs a <script>
tag, utilizing the src
of the script
tag, which is not restricted by the same-origin policy. The back-end URL
is filled in the src
, and a callback function is added to process the data obtained.
Referencing Ruan Yifeng's blog, the implementation idea is roughly as follows:
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = src;
document.body.appendChild(script);
}
window.onload = function () {
addScriptTag('http://example.com/ip?callback=foo');
}
function foo(data) {
console.log('response data: ' + JSON.stringify(data));
};
Since the script requested by the
<script>
element runs directly as code, as long as the browser defines thefoo
function, it will be called immediately. The JSON data passed as a parameter is treated as a JavaScript object rather than a string, thus avoiding the need forJSON.parse
.
How to implement in AJAX:
$.ajax({
url: 'http://www.domain2.com:8080/login',
type: 'get',
dataType: 'jsonp', // Request type is jsonp
jsonpCallback: "handleCallback", // Custom callback function name
data: {}
});
How to implement in Vue:
this.$http.jsonp('http://www.domain2.com:8080/login', {
params: {},
jsonp: 'handleCallback'
}).then((res) => {
console.log(res);
})
From the implementation principle, it can be seen that JSONP still has drawbacks, namely that using JSONP must be a GET request. If a POST request is needed to achieve cross-domain, other methods are still required.
2. WebSocket#
WebSocket
itself is a communication protocol. By communicating through WebSocket
, it is possible to bypass the same-origin policy, achieving a certain sense of "same-origin."
Below is the HTTP header information for a WebSocket
request, with a focus on the Origin
field, which is key to achieving cross-domain:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
The Origin field indicates the source of the request. As long as the source domain name in the Origin field matches the destination domain name of the request, cross-domain can be achieved through the same-origin policy.
If communication is allowed, the WebSocket response header is as follows:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
3. CORS#
CORS stands for "Cross-Origin Resource Sharing," which allows browsers to issue XMLHttpRequest requests to cross-origin servers, thus overcoming the limitation that AJAX can only be used with the same origin.
This is a common method for solving cross-domain issues.
Implementation Principle#
The implementation principle is illustrated as follows:
CORS requests are mainly divided into two categories: simple requests and non-simple requests.
Requests that meet the following conditions are considered simple requests; otherwise, they are non-simple requests:
(1) The request method is one of the following three methods:
HEAD
GET
POST
(2) The HTTP header information does not exceed the following fields:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type
: limited to three values:application/x-www-form-urlencoded
,multipart/form-data
,text/plain
Simple CORS requests simply add the Origin field to the HTTP header during the request.
For non-simple CORS requests, the browser will first send a preflight request after the formal communication to ask the server whether the request is allowed. After receiving a response and checking the relevant fields, it can respond and initiate the formal request.
Assuming we now initiate a JS script:
var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
The preflight request method is OPTIONS, and the specific request header is similar to the following:
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...
Key fields to focus on:
-
Origin
Indicates which source the request comes from.
-
Access-Control-Request-Method
This field is mandatory and lists which HTTP methods will be used in the browser's CORS request. In this example, it is
PUT
. -
Access-Control-Request-Headers
This field is a comma-separated string that specifies the header fields that the browser's CORS request will additionally send. In this example, it is
X-Custom-Header
.
After receiving a response like the following, cross-domain requests can be confirmed as allowed:
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
Where:
// Indicates support for any cross-domain request
Access-Control-Allow-Origin: *
// Indicates supported methods for cross-domain requests
Access-Control-Allow-Methods: GET, POST, PUT
// Required when the browser request includes Access-Control-Request-Headers, indicating supported header fields
Access-Control-Allow-Headers: X-Custom-Header
// Allows sending cookies and authentication information
Access-Control-Allow-Credentials: true
// Specifies the validity period of this preflight request
Access-Control-Max-Age: 1728000
Implementation Method#
This mainly involves back-end operations, and here is an example using Java
Spring Boot
for cross-domain configuration:
@Configuration
public class CORSConfiguration {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedHeaders("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT","PATCH")
.maxAge(3600);
}
};
}
}
This essentially adds request headers during the server response. Spring Boot
also supports using annotations to add configurations on different controllers
.
4. Nginx Proxy Cross-Domain#
The principle here is simple; it essentially allows the front-end and back-end to be on the same origin. By using Nginx's reverse proxy, we can modify the request's domain and port, and also add cookie information to achieve cross-domain.
# 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 domain in cookie
index index.html index.htm;
# When using middleware like webpack-dev-server to proxy API requests to nginx, there is no browser involvement, so there are no same-origin restrictions, and the following cross-domain configuration can be disabled
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;
}
}
Additionally, the principle of using some middleware proxy methods is the same, so I won't elaborate further.
Analyze the Problem#
A Thorough Analysis#
The above solutions have been discussed at length, but ultimately we need to return to our problem. Now, we will start to address it directly. First, let's take another look at the error log:
Hmm? It seems a bit different from what I imagined. Common cross-domain issues should look like:
This looks suspicious?
Sure enough, after adding cross-domain settings on the back end, our error message remained unchanged.
At this point, we need to analyze the error message carefully (actually, this should be the first step in analyzing logs, but to forcefully introduce cross-domain solutions, I specifically placed the analysis at the end).
This sentence caught my attention:
Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.
After consulting the information, I found that my front-end and back-end interactions were implemented locally, and the HTML was opened using the file protocol. However, requests using the file protocol cannot be recognized by the browser. The method provided online is as follows:
In the shortcut location for Google Chrome:
Add the following to the target:
"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" -args --disable-web-security --user-data-dir --allow-file-access-from-files
That's roughly it, but I still do not recommend using this method, as it is not a particularly elegant solution.
Another Attempt#
Another solution is to deploy Nginx locally, as mentioned above, using the solutions that do not open files via the file protocol.
What is the file protocol opening method?
It looks something like this:
To switch to an HTTP-based opening method, it looks like this:
Also, if you don't mind the hassle, directly deploying the webpage to a server is also a solution.
Unexpected Results#
However!! Ultimately, the problem still wasn't resolved! This really stumped me...
There must be a reason. After more than an hour of relentless effort, I finally found the root of the problem.
————
The URL for the AJAX request must be in HTTP format...
Ahhh, it turns out I was too inexperienced...
Because I had just learned AJAX and was not familiar with its principles, it led to this problem in the end.
Reference Links#
AJAX Cross-Domain, This Should Be the Most Comprehensive Solution