The word “cross-domain” sticks like a sticking plaster to every front-end developer, whether you’re on the job or in an interview. In order to prepare for the interview, I would memorize several random plans every time. I don’t know why I do this, but I can throw them away after the reverse. I don’t think I will use so many random plans in my work. When it comes to the real work, the development environment will be done by Webpack-dev-server, and the big guys on the server side will also be matched. I don’t care what is matched, but it won’t cross domains. So the days went by, and finally one day I decided I couldn’t go on like this any longer. I had to get to the bottom of this thing! Hence the article.
To master cross-domain, first of all, why does cross-domain exist
Indeed, we this kind of brick workers is to mix food to eat, a good interface to tell me across the domain, this obstruction we easily move the brick thing is disgusting! Why cross domains? Who’s doing this? To find out who caused this problem, click on your browser’s same-origin policy. It’s hard to understand the official stuff, but at least you know it, because the same origin policy of the browser causes cross-domain, and that’s what the browser is doing. So why do browsers mess up? Just don’t want to give us good days? In response to this query, the browser said, “The same origin policy restricts how documents or scripts loaded from the same source can interact with resources from another source. This is an important security mechanism for isolating potentially malicious files.” It’s hard to understand the official language, but it doesn’t matter, at least you know, it seems to be a security mechanism. So why do we need such a security mechanism at all? What problem does such a security mechanism solve? Don’t worry. Let’s get on with our research.
Two dangerous scenarios without the same-origin policy restriction
As FAR as I know, browsers do this same origin policy from two aspects: one is for interface requests and the other is for Dom queries. Imagine the danger of both of these actions without such restrictions.
The interface request is not restricted by the same origin policy
There’s a little thing called a cookie that you probably know, and it’s used for things like login, and the purpose is to let the server know who made the request. If you request the interface for login, the server will add the set-cookie field in the response header after verification. Then, the browser will automatically attach the Cookie to the HTTP request header field in the next request, so that the server can know that the user has logged in. After knowing this, let’s look at the scenario: 1. You are going to empty your shopping cart, so you open maimaimai.com www.maimaimai.com, and log in successfully. 2. You’re looking for something to buy when your best gay friend sends you a link to www.nidongde.com and says with a smile, “You know what I mean.” You open it without hesitation. 3. You delightfully browse www.nidongde.com, but the site does something indescribable on the sly! Without the same origin policy restrictions, it made a request to www.maimaimai.com! Smart you must think of the above words “the server will be authenticated after the response header into the set-cookie field, and then the next time to send a request, the browser will automatically attach the Cookie in the HTTP request header field Cookie”, so that the illegal website is equivalent to logging in your account, can do whatever you want! If it’s not a Maimaimai account, but your bank account, then… This is the legendary CSRF attack talk about CSRF attack mode. Looking at this CSRF attack, I was thinking that even with the same-origin policy restriction, cookies are in clear text, so they can still be removed. So I read some cookie-related articles and talked about cookie, cookie /Session mechanism and security. I learned that the server can set httpOnly so that the front-end can not operate cookies. If there is no such setting, Like XSS attacks to get the XSS of cookieWeb security tests; If secure is set to secure, HTTPS encrypted communication is transmitted to prevent interception.
Dom queries without same-origin policy restrictions
1. One day you wake up and receive an email telling you that your bank account is at risk. Please click www.yinghang.com to change your password. You frighten urine, hurriedly point go in, or familiar bank login interface, you decisive input your account password, login go in to see money have little. 2. You don’t see clearly, you usually visit the bank website www.yinhang.com, but now you visit www.yinghang.com, what does this phishing website do?
1
2
3
4
5
6
7
|
// HTML
<iframe name=”yinhang” src=”www.yinhang.com”></iframe>
// JS
// Since there is no same-origin policy restriction, phishing sites can directly access the Dom of other sites
const iframe = window.frames[‘yinhang’]
Const node = iframe. Document. GetElementById (‘ you enter the password Input)
Console. log(‘ got this ${node}, can’T I get the password you just entered? ‘)
|
From this, we know that the same origin policy can avoid some risks, not to say that the same origin policy is safe, but that the same origin policy is a basic security mechanism of the browser, after all, can increase the cost of attack. In fact, there is no penetrable shield, but the cost of the attack is not proportional to the benefits obtained after the successful attack.
Correct way to open across domains
The same origin policy is a good thing for browsers to do. It’s a defense against evil attacks, but you can’t keep everyone out just to keep the bad guys out. Yeah, nice guys like us should be able to cross borders if we open it the right way. Here’s how to do it one by one, but before we do that, we need to do some preparatory work. To demonstrate cross-domain locally, we need to: 1. Run a random piece of front-end code (the following front-end is a random vUE) at http://localhost:9099. 2. Run a copy of the backend (node Koa2) at http://localhost:9971.
The correct opening mode of an interface request is restricted by the same Origin policy
JSONP in HTML tags, some tags such as script, img, such as access to resources are not cross-domain restrictions, to take advantage of this, we can do:
Write a small interface on the back end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// a tool to handle the return format of success and failure
const {successBody} = require(‘.. /utli’)
class CrossDomain {
static async jsonp (ctx) {
// Parameters passed from the front end
const query = ctx.request.query
// Set a cookie
ctx.cookies.set(‘tokenId’, ‘1’)
// query.cb is the name of the method that is returned by the back end to the front end. The back end returns a method that is executed immediately, and the data to be returned is placed in the parameters of the method.
ctx.body = `${query.cb}(${JSON.stringify(successBody({msg: query.msg}, ‘success’))})`
}
}
module.exports = CrossDomain
|
Simple front end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<html>
<head>
<meta charset=”utf-8″>
</head>
<body>
<script type=’text/javascript’>
// The back end returns the directly executed method, which is equivalent to executing the method. Since the back end puts the returned data in the parameter of the method, it can get the res here.
window.jsonpCb = function (res) {
console.log(res)
}
</script>
</body>
</html>
|
Briefly encapsulate the front end
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
/ * *
* JSONP request tool
* @param Specifies the requested url address
* @param data Request parameters
* @returns {Promise<any>}
* /
const request = ({url, data}) => {
return new Promise((resolve, reject) => {
// process the input as xx=yy&aa=bb
const handleData = (data) => {
const keys = Object.keys(data)
const keysLen = keys.length
return keys.reduce((pre, cur, index) => {
const value = data[cur]
const flag = index ! == keysLen – 1 ? ‘&’ : ‘ ‘
return `${pre}${cur}=${value}${flag}`
}, ‘ ‘)
}
// Dynamically create script tags
const script = document.createElement(‘script’)
// Get the data returned by the interface
window.jsonpCb = (res) => {
document.body.removeChild(script)
delete window.jsonpCb
resolve(res)
}
script.src = `${url}? ${handleData(data)}&cb=jsonpCb`
document.body.appendChild(script)
})
}
// The usage mode
request({
url: ‘http://localhost:9871/api/jsonp’,
data: {
/ / the refs
msg: ‘helloJsonp’
}
}).then(res => {
console.log(res)
})
|
JSONP can only send a GET request, because the script load resource is essentially a GET request, so what if I send a POST request?
Write a small interface on the back end
1
2
3
4
5
6
7
8
9
10
|
// a tool to handle the return format of success and failure
const {successBody} = require(‘.. /utli’)
class CrossDomain {
static async iframePost (ctx) {
let postData = ctx.request.body
console.log(postData)
ctx.body = successBody({postData: postData}, ‘success’)
}
}
module.exports = CrossDomain
|
The front end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
const requestPost = ({url, data}) => {
// Create an iframe to send data to.
const iframe = document.createElement(‘iframe’)
iframe.name = ‘iframePost’
iframe.style.display = ‘none’
document.body.appendChild(iframe)
const form = document.createElement(‘form’)
const node = document.createElement(‘input’)
// Register the iframe load event handler if you need to do something when the response returns.
iframe.addEventListener(‘load’, function () {
console.log(‘post success’)
})
form.action = url
// Execute the form in the specified iframe
form.target = iframe.name
form.method = ‘post’
for (let name in data) {
node.name = name
node.value = data[name].toString()
form.appendChild(node.cloneNode())
}
// Form elements need to be added to the main document.
form.style.display = ‘none’
document.body.appendChild(form)
form.submit()
// After the form is submitted, the form can be deleted without affecting the next data delivery.
document.body.removeChild(form)
}
// The usage mode
requestPost({
url: ‘http://localhost:9871/api/iframePost’,
data: {
msg: ‘helloIframePost’
}
})
|
3.CORS
CORS is a W3C standard, full name is “cross-origin Resource Sharing” (CROSS-domain resource sharing) CORS details. As the name suggests, this is standard practice for dealing with cross-domain problems. CORS has two types of requests, simple and non-simple.
Here is a reference to the above link ruan Yifeng teacher’s article to explain the simple request and non-simple request. 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) Request method is one of the following 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
1. Simple request backend
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// a tool to handle the return format of success and failure
const {successBody} = require(‘.. /utli’)
class CrossDomain {
static async cors (ctx) {
const query = ctx.request.query
// * Cookies are not carried in HTTP requests
ctx.set(‘Access-Control-Allow-Origin’, ‘*’)
ctx.cookies.set(‘tokenId’, ‘2’)
ctx.body = successBody({msg: query.msg}, ‘success’)
}
}
module.exports = CrossDomain
|
The front end doesn’t have to do anything, just make normal requests, and if cookies are required, both the front and back ends need to be set, as you’ll see in the non-simple request example below.
1
2
3
|
fetch(`http://localhost:9871/api/cors? msg=helloCors`).then(res => {
console.log(res)
})
|
2. Non-simple requests
A non-simple request will issue a precheck request with a return code of 204. The request will not actually be issued until the precheck passes, and 200 will be returned. A non-simple request is triggered by adding an extra HEADERS to the request.
The back-end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// a tool to handle the return format of success and failure
const {successBody} = require(‘.. /utli’)
class CrossDomain {
static async cors (ctx) {
const query = ctx.request.query
// To enable cookies in HTTP requests, set the credentials on both the front and back ends, and set the specified Origin on the back end
ctx.set(‘Access-Control-Allow-Origin’, ‘http://localhost:9099’)
ctx.set(‘Access-Control-Allow-Credentials’, true)
// For CORS requests that are not simple requests, an HTTP query request is added before formal communication, called a “preflight” request.
Access-control-request-method = access-Control-request-headers = access-Control-request-headers = access-Control-request-headers
ctx.set(‘Access-Control-Request-Method’, ‘PUT,POST,GET,DELETE,OPTIONS’)
ctx.set(‘Access-Control-Allow-Headers’, ‘Origin, X-Requested-With, Content-Type, Accept, t’)
ctx.cookies.set(‘tokenId’, ‘2’)
ctx.body = successBody({msg: query.msg}, ‘success’)
}
}
module.exports = CrossDomain
|
With so much code for one interface, what’s a more elegant way to treat all interfaces the same? See KOA2-CORS below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
const path = require(‘path’)
const Koa = require(‘koa’)
const koaStatic = require(‘koa-static’)
const bodyParser = require(‘koa-bodyparser’)
const router = require(‘./router’)
const cors = require(‘koa2-cors’)
const app = new Koa()
const port = 9871
app.use(bodyParser())
Here is the directory after the front end is built
app.use(koaStatic(
path.resolve(__dirname, ‘.. /dist’)
))
/ / processing cors
app.use(cors({
origin: function (ctx) {
return ‘http://localhost:9099’
},
credentials: true,
allowMethods: [‘GET’, ‘POST’, ‘DELETE’],
allowHeaders: [‘t’, ‘Content-Type’]
}))
/ / routing
app.use(router.routes()).use(router.allowedMethods())
// Listen on the port
app.listen(9871)
console.log(`[demo] start-quick is starting at port ${port}`)
|
The front end
1
2
3
4
5
6
7
8
9
10
|
fetch(`http://localhost:9871/api/cors? msg=helloCors`, {
// Cookie is required
credentials: ‘include’,
// Add additional headers here to trigger non-simple requests
headers: {
‘t’: ‘extra headers’
}
}).then(res => {
console.log(res)
})
|
4. Proxy: If we still use the front-end domain name when requesting a domain name, and then something forwards the request to the real back-end domain name for us, wouldn’t cross domains be avoided? This is where Nginx comes in. Nginx configuration
1
2
3
4
5
6
7
8
9
10
|
server{
Listen on port 9099
listen 9099;
Localhost = localhost
server_name localhost;
If localhost:9099/ API is used, send it to http://localhost:9871
location ^~ /api {
proxy_pass http://localhost:9871;
}
}
|
There’s nothing to do on the front end, nothing to do on the back end except write interfaces, right
1
2
3
4
5
6
7
8
9
10
11
|
// Use the domain name http://localhost:9099 directly in the request, so that it does not cross domains. Then Nginx listens for any localhost:9099/ API, and forwards it to the real server address http://localhost:9871
fetch(‘http://localhost:9099/api/iframePost’, {
method: ‘POST’,
headers: {
‘Accept’: ‘application/json’,
‘Content-Type’: ‘application/json’
},
body: JSON.stringify({
msg: ‘helloIframePost’
})
})
|
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.
The correct way to open A Dom query under the same origin policy
1. PostMessage window.postMessage() is an HTML5 interface that focuses on implementing cross-domain communication between different Windows and different pages. For demonstration purposes, let’s change hosts to 127.0.0.1 crossDomain.com. Now visiting the domain name crossDomain.com is the same as accessing 127.0.0.1.
This is http://localhost:9099/#/crossDomain, the sender
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
<template>
<div>
< button @ click = “postMessage” > send the message to http://crossDomain.com:9099 < / button >
<iframe name=”crossDomainIframe” src=”http://crossdomain.com:9099″></iframe>
</div>
</template>
<script>
export default {
mounted () {
window.addEventListener(‘message’, (e) => {
// The source must be verified
if (e.origin === ‘http://crossdomain.com:9099’) {
/ / from http://crossdomain.com:9099 reply results
console.log(e.data)
}
})
},
methods: {
/ / send message to http://crossdomain.com:9099
postMessage () {
const iframe = window.frames[‘crossDomainIframe’]
Iframe. PostMessage (‘ I am [http://localhost:9099], please check your side have any id for the app Dom ‘, ‘http://crossdomain.com:9099’)
}
}
}
</script>
|
This is crossDomain.com :9099, the receiver
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<template>
<div>
I’m a http://crossdomain.com:9099
</div>
</template>
<script>
export default {
mounted () {
window.addEventListener(‘message’, (e) => {
// The source must be verified
if (e.origin === ‘http://localhost:9099’) {
// A message from http://localhost:9099
console.log(e.data)
// e.ource can be the reply object, which is a reference to the http://localhost:9099 window object
// e.olin can be used as a targetOrigin
E.s ource. PostMessage (` I am [http://crossdomain.com:9099], I know brother, this is what you want to know the results: ${document.getelementById (‘app’)? ‘:’ no Dom’} ‘, e.olin);
}
})
}
}
</script>
|
The result can be seen:
2. Document. domain This mode applies only to iframes with the same primary domain name but different subdomain names. Such as the main domain name is crossdomain.com: 9099, subdomain is child.crossdomain.com: 9099, In this case, the two pages can access their respective window objects by specifying document.domain = crossDomain.com.
3. Cross-domain problem of Canvas operating pictures. This should be a relatively rare cross-domain problem