Введение интерфейсных междоменных решений

JavaScript

предисловие

На самом деле есть много статей о кросс-доменном интерфейсе, но большинство из них все еще не соответствует моему аппетиту. Думается, что если вы хотите впечатлить себя своим пониманием, вам все равно придется самому его стучать, и подводить итоги, и организовывать блог для его записи.

Междоменные ограничения

Кросс-доменный, чтобы пользователи не могли читать контент под другим доменным именем, Ajax может получить ответ, браузер считает, что это небезопасно, поэтому он перехватывает ответ.

междоменное решение

Если не указано иное, html-файлы, отмеченные ниже, запускаются по умолчанию вhttp://127.0.0.1:5500на службе

CORS

CORS расшифровывается как Cross-Origin Resource Sharing. Это позволяет браузерам выполнять Ajax-запросы к серверам из разных источников, тем самым преодолевая ограничение, согласно которому Ajax можно использовать только из одного и того же источника. Кросс-доменный таким образом в основном устанавливается на бэкенде.

Ключом к этому методу является установка серверной части, то есть серверная часть открывает Access-Control-Allow-Origin как*или соответствующийoriginМожет быть достигнут междоменный домен.

Браузеры делят запросы CORS на две категории: простые запросы и непростые запросы.

Пока следующие два условия выполняются одновременно, это простой запрос.

  1. Метод запроса является одним из трех методов:
  • HEAD
  • GET
  • POST
  1. Информация заголовка HTTP не превышает следующих полей:
  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type: ограничено тремя значениями application/x-www-form-urlencoded, multipart/form-data, text/plain

Если два вышеуказанных условия не выполняются одновременно, это непростая заявка.

простой запрос

cors.html

let xhr = new XMLHttpRequest() xhr.open('GET', 'http://localhost:8002/request')
xhr.send(null)

server.js

const express = require('express')
const app = express()

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'http://127.0.0.1:5500') // 设置允许哪个域访问
  next()
})

app.get('/request', (req, res) => {
  res.end('server ok')
})

app.listen(8002)

не простой запрос

Вышеупомянутый простой запрос.Если мы используем не простой метод запроса, такой как метод запроса PUT, мы также можем реализовать междоменный, установив.

Для запросов CORS, которые не являются простыми запросами, перед формальным общением будет добавлен запрос HTTP-запроса, который называется предварительным запросом.

Браузер сначала запрашивает сервер. После того, как сервер получает «предварительный» запрос, он проверяет поля Origin, Access-Control-Request-Method и Access-Control-Request-Headers и подтверждает, что запросы между источниками разрешены, прежде чем браузер отправляет формальный запрос XMLHttpRequest, иначе будет сообщено об ошибке.

let xhr = new XMLHttpRequest()
xhr.open('PUT', 'http://localhost:8002/request')
xhr.send(null)

server.js

const express = require('express')
const app = express()

let whileList = ['http://127.0.0.1:5500'] // 设置白名单
app.use((req, res, next) => {
  let origin = req.headers.origin
  console.log(whitList.includes(origin))
  if (whitList.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin) // 设置允许哪个域访问
    res.setHeader('Access-Control-Allow-Methods', 'PUT') // 设置允许哪种请求方法访问
  }
  next()
})

app.put('/request', (req, res) => {
  res.end('server ok')
})

app.listen(8002)

Весь процесс отправил два запроса, и кросс-домен прошел успешно.

Конечно, можно задать и другие параметры:

  • Access-Control-Request-Headers

Это поле представляет собой строку с разделителями-запятыми, указывающую дополнительные поля заголовка, которые будут отправлять запросы CORS браузера.

  • Access-Control-Allow-Credentials

Указывает, разрешить ли отправку файлов cookie. По умолчанию файлы cookie не включаются в запросы CORS.

  • Access-Control-Expose-Headers

Когда запрос представляет собой CORS, метод GetResponseheader XMLHTTREQUEST Object (), чтобы получить только шесть основных полей: Cache-Control, язык контента, тип контента, истекает, последнее модифицированное, прагма. Если вы хотите получить другие поля, должны быть указаны в заголовках Excosse-Control-Expose-Control.

  • Access-Control-Max-Age

Используется для указания периода действия этого запроса предварительной проверки в секундах. Срок действия составляет 20 дней (1728000 секунд), то есть ответ разрешается кэшировать на 1728000 секунд (20 дней), в течение которых не выдается очередной предварительный запрос.

Прокси промежуточного программного обеспечения узла

Принцип реализации: Политика одного и того же источника является стандартом, которому должны следовать браузеры, и если это запрос между серверами, междоменный запрос отсутствует.

Прокси-сервер требует следующих шагов:

  1. Принимать запросы клиентов.
  2. Перенаправить запрос на сервер.
  3. Получите данные ответа сервера.
  4. Передать ответ клиенту.

На этот раз мы используем экспресс-промежуточное ПО http-proxy-middleware для междоменного проксирования, пересылки запросов и ответов.

Все три файла дела находятся в каталоге одного уровня:

index.html

let xhr = new XMLHttpRequest()
xhr.open('GET', '/api/request')
xhr.onreadystatechange = () => {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log('请求成功,结果是:', xhr.responseText) // request success
  }
}
xhr.send(null)

nodeMdServer.js

const express = require('express')
const { createProxyMiddleware } = require('http-proxy-middleware')

const app = express()

// 设置静态资源
app.use(express.static(__dirname))

// 使用代理
app.use(
  '/api',
  createProxyMiddleware({
    target: 'http://localhost:8002',
    pathRewrite: {
      '^/api': '', // 重写路径
    },
    changeOrigin: true,
  })
)

app.listen(8001)

nodeServer.js

const express = require('express')
const app = express()

app.get('/request', (req, res) => {
  res.end('request success')
})

app.listen(8002)

бегатьhttp://localhost:8001/index.html, междоменный успех

Обычно, когда проект vue/react настраивает webpack-dev-server, это тоже решается с помощью Node proxy.

Обратный прокси Nginx

Принцип реализации аналогичен прокси Node промежуточного ПО, который требует создания транзитного сервера nginx для пересылки запросов.

Таким образом, вам нужно только изменить конфигурацию Nginx для решения междоменной проблемы.Фронтенд заменяется соответствующей формой, кроме интерфейса, и тогда фронтенд и бэкэнд не нужно модифицирован для других модификаций.

Идея реализации: Настроить прокси-сервер (разные порты в одном домене) как трамплин через nginx, а реверс-прокси на междоменное доменное имя, чтобы информацию о домене в куках можно было модифицировать, что удобно для записи текущего файл cookie домена и реализует междоменный вход в систему.

nginx.conf в каталоге nginx изменяется следующим образом:

// proxy服务器
server {
    listen 80;
    server_name  www.domain1.com;
    location / {
        proxy_pass   http://www.domain2.com:8080;  # 反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; # 修改cookie里域名
        index  index.html index.htm;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  # 当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

Запустить Nginx

index.html

var xhr = new XMLHttpRequest()
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true
// 访问nginx中的代理服务器
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))
  // 向前台写cookie
  res.writeHead(200, {
    'Set-Cookie': 'l=123456;Path=/;Domain=www.domain2.com;HttpOnly', // HttpOnly:脚本无法读取
  })
  res.write(JSON.stringify(params))
  res.end()
})
server.listen(8080)

jsonp

Принцип: Используя междоменную функцию тега script, определите функцию обратного вызова на стороне клиента (глобальная функция), запросить у сервера возврат вызова callback-функции, и передать данные сервера в виде параметров callback-функции, после чего функция выполняется. Этот метод требует сотрудничества сервера для завершения.

Этапы реализации:

  1. Объявите глобальную функцию обратного вызова, параметром будут данные, возвращаемые сервером.
  2. Создайте тег сценария, соедините адрес всего API-интерфейса запроса (передайте имя функции обратного вызова, например ?callback=getInfo), и назначьте его атрибуту src сценария.
  3. После получения запроса сервер обрабатывает данные, а затем объединяет имя функции и возвращаемые данные в строку, сборка завершается в виде выполнения функции. (getInfo('данные сервера'))
  4. Браузер получает возвращаемый результат от сервера и вызывает объявленную функцию обратного вызова.

jsonp.html

function getInfo(data) {
  console.log(data) // 告诉你一声, jsonp跨域成功
}

let script = document.createElement('script')
script.src = 'http://localhost:3000?callback=getInfo' //
document.body.appendChild(script)

server.js

const express = require('express')
const app = express()

app.get('/', (req, res) => {
  let { callback } = req.query
  res.end(`${callback}('告诉你一声, jsonp跨域成功')`)
})

app.listen(3000)

Метод jQuery $ .ajax(), который интегрирует реализацию JSONP, не выписывается.

В процессе разработки можно столкнуться с тем, что имена функций обратного вызова для нескольких запросов JSONP совпадают, и этот метод также проблематичен в использовании, поэтому мы сами инкапсулируем функцию jsonp.

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 = { callback, ...params } // {callback: "getInfo", name: "jacky"}
    let paramsArr = []
    for (const key in params) {
      paramsArr.push(`${key}=${params[key]}`)
    }
    script.src = `${url}?${paramsArr.join('&')}` // http://localhost:3000/?callback=getInfo&name=jacky
    document.body.appendChild(script)
  })
}

jsonp({
  url: 'http://localhost:3000',
  params: {
    name: 'jacky',
  },
  callback: 'getInfo',
}).then(res => {
  console.log(res) // 告诉你一声, jsonp跨域成功
})

Параметры могут быть удалены при деконструкции сервера

app.get('/', (req, res) => {
  let { callback, name } = req.query
  res.end(`${callback}('告诉你一声, jsonp跨域成功')`)
})

Достоинства: хорошая совместимость

Недостаток: из-за ограничений самого скрипта этот междоменный метод поддерживает только запросы на получение, он небезопасен и может подвергаться XSS-атакам.

postMessage

postMessage — это API в HTML5 XMLHttpRequest Level 2 и одно из немногих свойств окна, которые могут работать между доменами. Его можно использовать для решения следующих проблем:

  1. Передача данных страницы и новое окно, которое она открывает
  2. Передача сообщений между несколькими окнами
  3. Страницы и вложенные обмена сообщениями IFrame
  4. Междоменная передача данных в описанных выше трех сценариях

Короче говоря, он позволяет сценариям из разных источников взаимодействовать асинхронно с ограниченным взаимодействием, обеспечивая кросс-текстовый документ, многооконный и междоменный обмен сообщениями.

otherWindow.postMessage(message, targetOrigin, [transfer]);

  • otherWindow: ссылка на другое окно, например свойство contentWindow элемента iframe, объект окна, возвращаемый при выполнении команды window.open, или именованный или пронумерованный численно индекс window.frames.
  • message: Данные для отправки в другие окна.
  • targetOrigin: укажите, какие окна могут получать события сообщения через свойство происхождения окна, и его значение может быть строкой «*» (представляющей неограниченное количество) или URI. При отправке сообщения, если какой-либо протокол, адрес хоста или порт целевого окна не соответствует значению, предоставленному targetOrigin, сообщение не будет отправлено; сообщение будет отправлено только в том случае, если все три точно совпадают.
  • передача (необязательно): это строка объектов Transferable, которые передаются одновременно с сообщением. Право собственности на эти объекты будет передано получателю сообщения, а отправитель больше не сохранит право собственности.

В этот раз вешаем два html файла под два сервера, импортируем их чтением fs и запускаем два js файла

postMessage1.html

<body>
  <iframe src="http://localhost:8002" frameborder="0" id="frame" onLoad="load()"></iframe>
  <script>
    function load() {
      let frame = document.getElementById('frame')
      frame.contentWindow.postMessage('你好,我是postMessage1', 'http://localhost:8002') //发送数据
      window.onmessage = function (e) {
        //接受返回数据
        console.log(e.data) // 你好,我是postMessage2
      }
    }
  </script>
</body>

postMsgServer1.js

const express = require('express')
const fs = require('fs')
const app = express()

app.get('/', (req, res) => {
  const html = fs.readFileSync('./postMessage1.html', 'utf8')
  res.end(html)
})

app.listen(8001, (req, res) => {
  console.log('server listening on 8001')
})

postMessage2.html

<body>
  <script>
    window.onmessage = function (e) {
      console.log(e.data) // 你好,我是postMessage1
      e.source.postMessage('你好,我是postMessage2', e.origin)
    }
  </script>
</body>

postMsgServer2.js

const express = require('express')
const fs = require('fs')
const app = express()

app.get('/', (req, res) => {
  const html = fs.readFileSync('./postMessage2.html', 'utf8')
  res.end(html)
})

app.listen(8002, (req, res) => {
  console.log('server listening on 8002')
})

websocket

WebSocket — это сетевой протокол связи. Он реализует полнодуплексную связь между браузером и сервером и одновременно позволяет осуществлять междоменную связь, а метод длительного соединения не зависит от междоменного соединения. Поскольку собственный API WebSocket неудобен в использовании, мы обычно используем сторонние библиотеки, такие как ws.

И веб-браузеры, и серверы должны реализовывать протокол WebSockets для установления и поддержания соединений. Поскольку соединения WebSockets долговечны, в отличие от обычных HTTP-соединений, это имеет важные последствия для сервера.

сокет.html (http://127.0.0.1:5500/socket.html)

let socket = new WebSocket('ws://localhost:8001')
socket.onopen = function () {
  socket.send('向服务端发送数据')
}
socket.onmessage = function (e) {
  console.log(e.data) // 服务端传给你的数据
}

бегатьnodeServer.js

const express = require('express')
const WebSocket = require('ws')
const app = express()

let wsServer = new WebSocket.Server({ port: 8001 })
wsServer.on('connection', function (ws) {
  ws.on('message', function (data) {
    console.log(data) // 向服务端发送数据
    ws.send('服务端传给你的数据')
  })
})

document.domain + iframe

Этот метод можно использовать только в том случае, если доменное имя второго уровня совпадает.

Например, a.test.com и b.test.com являются поддоменами test.com.

Вам нужно только добавить document.domain = 'test.com' на страницу, чтобы указать, что доменное имя второго уровня одинаково для достижения междоменного взаимодействия.

Например: страница a.test.com:3000/test1.html получает значение a на странице b.test.com:3000/test2.html.

test1.html

<body>
  <iframe
    src="http://b.test.com:3000/test2.html"
    frameborder="0"
    onload="load()"
    id="iframe"
  ></iframe>
  <script>
    document.domain = 'test.com'
    function load() {
      console.log(iframe.contentWindow.a)
    }
  </script>
</body>

test2.html

document.domain = 'test.com'
var a = 10

window.name + iframe

В браузерах есть функция, согласно которой страницы, загружаемые на одной вкладке или в одном фрейме, имеют одно и то же значение свойства window.name. В той же вкладке значение имени все еще существует после загрузки разных страниц, и значение свойства window.name на этих страницах одинаково. Используя эти свойства, можно использовать это свойство в качестве среды для передачи данных между разными страницами.

Из соображений безопасности браузеры всегда сохраняют window.name строкового типа.

Открытьhttp://localhost:8001/a.html

<body>
  <iframe
    src="http://localhost:8002/c.html"
    frameborder="0"
    onload="load()"
    id="iframe"
  ></iframe>
  <script>
    let first = true
    function load() {
      if (first) {
        // 第1次onload(跨域页)成功后,切换到同域代理页面
        let iframe = document.getElementById('iframe')
        iframe.src = 'http://localhost:8001/b.html'
        first = false
      } else {
        // 第2次onload(同域b.html页)成功后,读取同域window.name中数据
        console.log(iframe.contentWindow.name) // 我是c.html里的数据
      }
    }
  </script>
</body>
<body>
  <script>
    window.name = '我是c.html里的数据'
  </script>
</body>

Страница с устанавливает значение для window.name.Даже если страница с будет уничтожена, значение имени не будет уничтожено, страница а все еще может получить окно.имя.

location.hash + iframe

Принцип реализации: если A.HTML хочет общаться с C.HTML через домены, он реализуется через промежуточную страницу B.HTML. Три страницы, разные домены используют местоположение Iframe. Hash Pass Neighle и прямой доступ JS между теми же доменами для связи.

Конкретные этапы реализации: сначала a.html отправляет хеш-значение в c.html, затем c.html получает хеш-значение, затем передает хэш-значение в b.html, и, наконец, b.html помещает результат в a.html в значении хеша.

Точно так же a.html и b.html находятся в одном домене, обаhttp://localhost:8001, то естьХэш-значение b может быть напрямую скопировано в хеш-код a. c.html этопод http://localhost:8002

a.html

<body>
  <iframe src="http://localhost:8002/c.html#jackylin" style="display: none;"></iframe>
  <script>
    window.onhashchange = function () {
      // 检测hash的变化
      console.log(456, location.hash) // #monkey
    }
  </script>
</body>

b.html

window.parent.parent.location.hash = location.hash
// b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面

c.html

console.log(location.hash) //  #jackylin
let iframe = document.createElement('iframe')
iframe.src = 'http://localhost:8001/b.html#monkey'
document.body.appendChild(iframe)

Суммировать

  • CORS поддерживает все HTTP-запросы, что является основной междоменной программой.
  • В повседневной работе наиболее часто используемыми междоменными решениями являются промежуточное ПО CORS и Node, а также обратный прокси-сервер Nginx.
  • Будь то прокси-сервер промежуточного слоя Node или обратный прокси-сервер Nginx, сервер не ограничивается в основном политикой одного и того же источника.

Ссылаться на