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?

HTML <iframe name="yinhang" SRC ="www.yinhang.com"></iframe> Phishing site can be directly to other sites Dom const iframe = window. The 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? ')Copy the code

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 code (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

// 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}(The ${JSON.stringify(successBody({msg: query.msg}, 'success'))}) `}}module.exports = CrossDomain

Copy the code

Simple front end

<! DOCTYPE html><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>
    <script src='http://localhost:9871/api/jsonp? msg=helloJsonp&cb=jsonpCb' type='text/javascript'></script>
  </body>
</html>

Copy the code

Briefly encapsulate the front end

/** * JSONP request tool *@param Url Specifies the requested address *@param Data Specifies the parameters of the request *@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]
        constflag = 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)
})
Copy the code

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

// 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
Copy the code

The front end

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'}})Copy the code

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

// 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'.The '*')
    ctx.cookies.set('tokenId'.'2')
    ctx.body = successBody({msg: query.msg}, 'success')}}module.exports = CrossDomain

Copy the code

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.

fetch(`http://localhost:9871/api/cors? msg=helloCors`).then(res= > {
  console.log(res)
})
Copy the code

2. Non-simple request A non-simple request will send a pre-check request with the return code of 204. The request will be sent only after the pre-check passes and 200 will be returned. A non-simple request is triggered by adding an extra HEADERS to the request.

The back-end

// 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
Copy the code

With so much code for one interface, what’s a more elegant way to treat all interfaces the same? See KOA2-CORS below.

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}`)

Copy the code

The front end

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)
})
Copy the code

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

Server {# listening in9099Port to listen9099;
    # 域名是localhost
    server_name localhost;
    #凡是localhost:9099/ API like this, all forward to the real server address HTTP://localhost:9871 
    location ^~ /api {
        proxy_pass http://localhost:9871;}}Copy the code

There’s nothing to do on the front end, nothing to do on the back end except write interfaces, right

// 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'})})Copy the code

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

<template>
  <div>
    <button @click="postMessage">Send a 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 whether you have a Dom with the ID of APP.'.'http://crossdomain.com:9099')}}}</script>
Copy the code

This is crossDomain.com :9099, the receiver

<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.source.postMessage(` I am [http://crossdomain.com:9099], I know brother, this is what you want to know the results:The ${document.getElementById('app')?I have a Dom with the id app. : 'No Dom with id app'}`, e.origin); }}}})</script>
Copy the code

The result can be seen:

2.document.domain

This mode applies only to iframes with the same primary domain name but different subdomain names.

Let’s say the main domain name isCrossdomain.com :9099, subdomain http://child.cros…= crossDomain.com can access the respective window objects.

3. Cross-domain problem of Canvas operating pictures. This should be a relatively rare cross-domain problem