перекрестный доменЭто проблема, которую невозможно избежать в области фронтенда.Сегодня давайте поговорим о междоменном фронтенде.
Та же политика происхождения
Та же политика происхождения (same-origin policy)Первоначально введен в 1995 году Netspace безопасной стратегией браузера, и теперь все браузеры соответствуют той же политике происхождения, которая является краеугольным камнем безопасности браузера.
Политика единого происхождения предусматривает, что сценарии в разных доменах изолированы друг от друга, а сценарии в одном домене не могут получить доступ и использовать большинство свойств и методов другого домена. Так называемыйгомологияОтноситсятот же протокол,такое же доменное имя,тот же порт.
Политика одного и того же происхождения изначально использовалась только для предотвращения доступа сценариев из разных доменов к файлам cookie, но с развитием Интернета политика одного и того же происхождения становится все более и более строгой. , DOM-контент, AJAX (асинхронный JavaScript и XML, асинхронные технологии JavaScript и XML) нельзя использовать в обычном режиме.
В следующей таблице приведен пример определения гомологии на примере http://www.a.com/page/index.html:
Пример | URL | результат | причина |
---|---|---|---|
A | http://www.a.com/page/login.html | успех | гомология |
B | http://www.a.com/page2/index.html | успех | гомология |
C | https://www.a.com/page/secure.html | потерпеть неудачу | разные протоколы |
D | http://www.a.com:8080/page/index.html | потерпеть неудачу | разные порты |
E | http://static.a.com/page/index.html | потерпеть неудачу | разные доменные имена |
F | http://www.b.com/page/index.html | потерпеть неудачу | разные доменные имена |
решение
Решение можно разделить на четыре основных аспекта в зависимости от решения:
- Чистый интерфейсный подход
- Чистый бэкэнд-подход
- Способ спереди и сзади
- другие методы
Чистый интерфейсный подход
- имеют
src
илиherf
метка атрибута - window.name
- document.domain
- location.hash
- postMessage
- CSST (CSS Text Transformation)
- Flash
имеютsrc
илиherf
метка атрибута
у всех естьsrc
Все теги атрибутов являются междоменными, например:<script>
,<img>
,<iframe>
,так же как<link>
Теги, эти теги дают нам возможность вызывать сторонние ресурсы.
Эти теги также имеют ограничения, такие как: могут использоваться только дляGET
Для получения ресурсов нужно создать DOM-объект и т. д.
Различные теги имеют разные механизмы отправки запросов и должны обрабатываться по-разному. Такие как:<img>
лейбл меняетсяsrc
Атрибуты запрашиваются, в то время как другие теги должны быть добавлены в дерево DOM до выполнения запросов.
const img = new Image()
img.src = 'http://domain.com/picture' // 发起请求
const iframe = document.createElement('iframe')
iframe.src = 'http://localhost:8082/window_name_data.html'
document.body.appendChild(iframe) // 发起请求
window.name
window.name
свойства иiframe
Междоменные возможности тегов. Значением window.name является не обычная глобальная переменная, а имя текущего окна, тэг iframe тоже имеет обернутую форму, и естественно присутствует атрибут window.name.
Магия свойства window.name заключается в том, что значение имени сохраняется на всех загруженных страницах (или даже в доменах) и не изменяется без модификации.
// 打开一个空白页,打开控制台
window.name = JSON.stringify({ name: 'window', version: '1.0.0' })
window.location = 'http://baidu.com'
//页面跳转且加载成功后, window.name 的值还是我们最初赋值的值
console.log(window.name) // {"name":"window","version":"1.0.0"}
Атрибут window.name в сочетании с междоменными возможностями iframe может реализовать передачу данных между разными доменами. Конкретные шаги заключаются в следующем:
- Динамически создавать тег iframe на странице посещения (http://a.com/page.html), а атрибут src указывает на страницу данных (http://b.com/data.html).
- Привяжите событие загрузки к iframe, когда страница данных успешно загружена, укажите атрибут src iframe на прокси-страницу того же происхождения (или пустую страницу).
- Когда iframe снова загружается, вы можете манипулировать свойством contentWindow.name объекта iframe, чтобы получить значение window.name, установленное страницей источника данных.
Уведомление: когда страница источника данных успешно загружена (то есть было назначено имя окна), вам необходимо указать src iframe на страницу того же источника (или пустую страницу) посещаемой страницы.about:blank;
), иначе чтениеiframe.contentWindow.name
Будет сообщено об ошибке из-за политики того же происхождения.
Существует также идея реализации для window.name, котораяПосле установки значения window.name страница данных переходит на адрес страницы того же происхождения, что и родительская страница, через js., в этом случае родительская страница может получить значение window.name, оперируя объектом дочерней страницы того же происхождения для достижения цели связи.
document.domain
Принцип: используя js, чтобы установить одно и то же для родительских и дочерних страниц фреймов.document.domain
значение для достижения цели связи между родительскими и дочерними страницами.
Ограничение: Его можно использовать только в том же сценарии, что и основной домен.
Тег iframe — это мощный тег, который позволяет загружать другие страницы внутри страницы.Если нет политики одинакового происхождения, наш веб-сайт практически небезопасен перед тегом iframe.
www.a.com
а такжеnews.a.com
считаются разными доменами, то страницы под ними могут быть вложены друг в друга и отображаться через теги iframe, но они не могут взаимодействовать друг с другом (данные и методы на страницах не могут быть прочитаны и вызваны).В это время мы можем использовать js, чтобы установить две страницы.document.domain
ценностьa.com
(т.е. их общий основной домен), браузер будет считать, что они находятся под одним доменом, они могут называть подход друг друга к общению.
// http://www.a.com/www.html
document.domain = 'a.com'
// 设置一个测试方法给 iframe 调用
window.openMessage = function () {
alert('www page message !')
}
const iframe = document.createElement('iframe')
iframe.src = 'http://news.a.com:8083/document_domain_news.html'
iframe.style.display = 'none'
iframe.addEventListener('load', function () {
// 如果未设置相同的主域,那么可以获取到 iframeWin 对象,但是无法获取 iframeWin 对象的属性与方法
const iframeWin = iframe.contentWindow
const iframeDoc = iframeWin.document
const iframeWinName = iframeWin.name
console.log('iframeWin', iframeWin)
console.log('iframeDoc', iframeDoc)
console.log('iframeWinName', iframeWinName)
// 尝试调用 getTestContext 方法
const iframeTestContext = iframeWin.getTestContext()
document.querySelector('#text').innerText = iframeTestContext
})
document.body.appendChild(iframe)
// http://news.a.com/news.html
document.domain = 'a.com'
// 设置 windon.name
window.name = JSON.stringify({ name: 'document.domain', version: '1.0.0' })
// 设置一些全局方法
window.getTestContext = function () {
// 尝试调用父页面的方法
if (window.parent) {
window.parent.openMessage()
}
return `${document.querySelector('#test').innerText} (${new Date()})`
}
location.hash
Принцип: Реализуйте общение на странице, изменив значение привязки в URL-адресе. URL имеет#abc
Для такой информации привязки изменения в этой части информации не будут генерировать новые запросы (но будут генерировать историю браузера), передавать данные, изменяя хеш-значение подстраницы, и получать сообщения, отслеживая изменение собственного хеш-значения URL.
Для обеспечения двусторонней связи между родительской и дочерней страницами в этом решении требуются три страницы: основная вызывающая страница, страница данных и прокси-страница. Это связано с тем, что главная вызывающая страница может изменить хеш-значение страницы данных, но страница данных не может передатьparent.location.hash
способ изменить хеш-значение родительской страницы (только браузеры IE и Chrome не разрешены), поэтому вы можете загрузить другую прокси-страницу только на странице данных (Прокси-страница находится в том же домене, что и основная вызывающая страница.), чтобы управлять методами и свойствами главной вызывающей страницы через прокси-страницу того же домена.
// http://www.a.com/a.html
const iframe = document.createElement('iframe')
iframe.src = 'http://www.b.com/b.html'
iframe.style.display = 'none'
document.body.appendChild(iframe)
setTimeout(function () {
// 向数据页传递信息
iframe.src = `${iframe.src}#user=admin`
}, 1000)
window.addEventListener('hashchange', function () {
// 接收来自代理页的消息(也可以让代理页直接操作主调用页的方法)
console.log(`page: data from proxy.html ---> ${location.hash}`)
})
// http://www.a.com/b.html
const iframe = document.createElement('iframe')
iframe.src = 'http://www.a.com/proxy.html'
iframe.style.display = 'none'
document.body.appendChild(iframe)
window.addEventListener('hashchange', function () {
// 收到主调用页传来的信息
console.log(`data: data from page.html ---> ${location.hash}`)
// 一些其他的操作
const data = location.hash.replace(/#/ig, '').split('=')
if (data[1]) {
data[1] = String(data[1]).toLocaleUpperCase()
}
setTimeout(function () {
// 修改子页 proxy.html iframe 的 hash 传递消息
iframe.src = `${iframe.src}#${data.join('=')}`
}, 1000)
})
// http://www.a.com/proxy.html
window.addEventListener('hashchange', function () {
console.log(`proxy: data from data.html ---> ${location.hash}`)
if (window.parent.parent) {
// 把数据代理给同域的主调用页(也可以直接调用主调用页的方法传递消息)
window.parent.parent.location.hash = location.hash
}
})
postMessage
PostMessage — это API в HTML5 XMLHTTPRequest Level 2, который можно безопасно реализовать, который можно использовать для решения следующих проблем:
- Передача данных страницы и новое окно, которое она открывает
- Передача сообщений между несколькими окнами
- Страницы и вложенные сообщения iframe
- Междоменная передача данных в описанных выше трех сценариях
Для конкретного использования postMessage, пожалуйста, обратитесь кwindow.postMessage, есть 2 очка для примечания:
- Метод postMessage прикрепляется к определенному объекту окна, такому как contentWindow iframe, и выполняется
window.open
Объект окна, возвращаемый оператором, и т. д. -
targetOrigin
Параметр может указывать, какие окна получают сообщение, содержать протокол + хост + номер порта или может быть установлен в подстановочный знак «*».
// http://www.a.com/a.html
const iframe = document.createElement('iframe')
iframe.src = 'http://www.b.com/b.html'
iframe.style.display = 'none'
iframe.addEventListener('load', function () {
const data = { user: 'admin' }
// 向 b.com 传送跨域数据
// iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.b.com')
iframe.contentWindow.postMessage(JSON.stringify(data), '*')
})
document.body.appendChild(iframe)
// 接受 b.com 返回的数据
window.addEventListener('message', function (e) {
console.log(`a: data from b.com ---> ${e.data}`)
}, false)
// http://www.b.com/b.html
window.addEventListener('message', function (e) {
console.log(`b: data from a.com ---> ${e.data}`)
const data = JSON.parse(e.data)
if (data) {
data.user = String(data.user).toLocaleUpperCase()
setTimeout(function () {
// 处理后再发回 a.com
// window.parent.postMessage(JSON.stringify(data), 'http://www.a.com')
window.parent.postMessage(JSON.stringify(data), '*')
}, 1000)
}
}, false)
CSST (CSS Text Transformation)
Принцип: С помощью CSS3content
Свойство позволяет передавать текст между доменами, которые доставляют контент.
По сравнению с JSONP он более безопасен и не требует выполнения межсайтовых сценариев.
Недостаток в том, что нет адаптации JSONP, и он может нормально работать только в браузерах, поддерживающих CSS3.
Конкретный контент можно просмотреть с помощьюCSSTК пониманию.
Flash
У Flash есть собственный набор политик безопасности, которые может использовать сервер.crossdomain.xmlФайл для объявления SWF-файлов домена, к которым можно получить доступ, использования Flash для выполнения прокси-сервера междоменных запросов и передачи результата ответа в javascript для реализации междоменной связи.
Чистый бэкэнд-подход
- Server Proxy
- CORS (обмен ресурсами между источниками)
Server Proxy
Политика единого происхождения нацелена на браузеры, а на протокол http/https это не влияет, поэтому кроссдоменную проблему можно решить с помощью Server Proxy.
Шаги реализации также относительно просты, в основном после того, как сервер получает запрос клиента, он перенаправляет конкретный междоменный запрос, оценивая URL-адрес (http, https), и возвращает результат прокси-сервера клиенту, чтобы достичь Цель междоменного.
// NodeJs
const http = require('http')
const server = http.createServer(async (req, res) => {
if (req.url === '/api/proxy_server') {
const data = 'user=admin&group=admin'
const options = {
protocol: 'http:',
hostname: 'www.b.com',
port: 8081,
path: '/api/proxy_data',
method: req.method,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(data),
},
}
const reqProxy = http.request(options, (resProxy) => {
res.writeHead(resProxy.statusCode, { 'Content-Type': 'application/json' })
resProxy.pipe(res) // 将 resProxy 收到的数据转发到 res
})
reqProxy.write(data)
reqProxy.end()
}
})
Прокси-сервер в NodeJs в основном используетсяhttp
модульныйrequest
метод иstream
изpipe
метод.
Выше приведена простейшая реализация прокси-сервера NodeJs. В реальной сцене необходимо учитывать более сложные ситуации. Для получения более подробной информации вы можете нажатьКак написать обратный прокси-сервер HTTPчтобы понять.
Узнать больше о:Принцип HTTP-прокси и его реализация (1) HTTP Прокси Принцип и внедрение (B)
CORS (обмен ресурсами между источниками)
Полное название CORS — «Обмен ресурсами между источниками» (Cross-origin resource sharing), что является стандартом W3C. пройти черезCORSКлючевая часть протокола для достижения междоменной связи заключается всервертак же какПоддержка браузера(IE не ниже IE10), весь процесс связи CORS автоматически завершается браузером.Для разработчиков взаимодействие CORS ничем не отличается от запроса AJAX того же происхождения.
Браузеры делят запросы CORS на две категории: простые запросы и не очень простые запросы. Более подробную информацию можно прочитать поУчитель Руан ИфэнизПодробное объяснение CORS для совместного использования ресурсов между доменамистатья для более глубокого понимания.
// server.js
// http://www.b.com/api/cors
const server = http.createServer(async (req, res) => {
if (typeof req.headers.origin !== 'undefined') {
// 如果是 CORS 请求,浏览器会在头信息中增加 origin 字段,说明请求来自于哪个源(协议 + 域名 + 端口)
if (req.url === '/api/cors') {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
res.setHeader('Access-Control-Allow-Credentials', true)
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, PUT, DELETE, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Cache-Control, X-Access-Token')
const resData = {
error_code: 0,
message: '',
data: null,
}
if (req.method === 'OPTIONS') {
// not-so-simple request 的 预请求
res.setHeader('status', 200)
res.setHeader('Content-Type', 'text/plain')
res.end()
return
} else if (req.method === 'GET') {
// simple request
Object.assign(resData, { data: { user: 'admin' } })
} else if (req.method === 'PUT') {
// not-so-simple
res.setHeader('Set-Cookie', ['foo=bar; HttpOnly', 'bar=baz; HttpOnly', 'y=88']) // 设置服务器域名 cookie
Object.assign(resData, { data: { user: 'ADMIN', token: req.headers['x-access-token'] } })
} else {
Object.assign(resData, { data: { user: 'woqu' } })
}
res.setHeader('status', 200)
res.setHeader('Content-Type', 'application/json')
res.write(JSON.stringify(resData))
res.end()
return
}
res.setHeader('status', 404)
res.setHeader('Content-Type', 'text/plain')
res.write(`This request URL '${req.url}' was not found on this server.`)
res.end()
return
}
})
// http://www.a.com/cors.html
setTimeout(function () {
console.log('CORS: simple request')
ajax({
url: 'http://www.b.com:8082/api/cors',
method: 'GET',
success: function (data) {
data = JSON.parse(data)
console.log('http://www.b.com:8082/api/cors: GET data', data)
document.querySelector('#test1').innerText = JSON.stringify(data)
},
})
}, 2000)
setTimeout(function () {
// 设置 cookie
document.cookie = 'test cookie value'
console.log('CORS: not-so-simple request')
ajax({
url: 'http://www.b.com:8082/api/cors',
method: 'PUT',
body: { user: 'admin' },
header: { 'X-Access-Token': 'abcdefg' },
success: function (data) {
data = JSON.parse(data)
console.log('http://www.b.com:8082/api/cors: PUT data', data)
document.querySelector('#test2').innerText = JSON.stringify(data)
},
})
}, 4000)
Путь спереди и сзади
- JSONP (JSON с дополнением)
JSONP (JSON с дополнением)
принцип:<script>
Теги могут загружать и выполнять сценарии в разных доменах.
JSONP — это простой и эффективный междоменный метод, который легко реализовать, однако из-за выполнения межсайтового скриптинга он относительно уязвим для атак CSRF (подделка межсайтовых запросов), что приводит к утечке конфиденциальной информации пользователя.<script>
Междоменный метод тегов ограничен, и данные можно получить только методом GET.
// server.js
// http://www.b.com/api/jsonp?callback=callback
const server = http.createServer((req, res) => {
const params = url.parse(req.url, true)
if (params.pathname === '/api/jsonp') {
if (params.query && params.query.callback) {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.write(`${params.query.callback}(${JSON.stringify({ error_code: 0, data: 'jsonp data', message: '' })})`)
res.end()
}
}
// ...
})
// http://www.a.com/jsonp.html
const script = document.createElement('script')
const callback = function (data) {
console.log('jsonp data', typeof data, data)
}
window.callback = callback // 把回调函数挂载到全局对象 window 下
script.src = 'http://www.b.com:8081/api/jsonp?callback=callback'
setTimeout(function () {
document.body.appendChild(script)
}, 1000)
другие методы
- WebSocket
- SSE (события, отправленные сервером)
WebSocket
WebSocket Protocol - это новый протокол HTML5. Это реализует полнодуплексную связь между браузером и сервером и обеспечивает перекрестный коммуникацию. Это хорошая реализация технологии Push Server.
// 服务端实现可以使用 socket.io,详见 https://github.com/socketio/socket.io
// client
const socket = new WebSocket('ws://www.b.com:8082')
socket.addEventListener('open', function (e) {
socket.send('Hello Server!')
})
socket.addEventListener('message', function (e) {
console.log('Message from server', e.data)
})
SSE (события, отправленные сервером)
Это событие отправки сервера SSE, поддержка CORS, основанная на CORS, обеспечивает междоменную связь.
// server.js
const server = http.createServer((req, res) => {
const params = url.parse(req.url, true)
if (params.pathname === '/api/sse') {
// SSE 是基于 CORS 标准实现跨域的,所以需要设置对应的响应头信息
res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
res.setHeader('Access-Control-Allow-Credentials', true)
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, PUT, DELETE, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Cache-Control, X-Access-Token')
res.setHeader('status', 200)
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
res.write('retry: 10000\n')
res.write('event: connecttime\n')
res.write(`data: starting... \n\n`)
const interval = setInterval(function () {
res.write(`data: (${new Date()}) \n\n`)
}, 1000)
req.connection.addListener('close', function () {
clearInterval(interval)
}, false)
return
}
})
// http://www.a.com:8081/sse.html
const evtSource = new EventSource('http://www.b.com:8082/api/sse')
evtSource.addEventListener('connecttime', function (e) {
console.log('connecttime data', e.data)
document.querySelector('#log').innerText = e.data
})
evtSource.onmessage = function(e) {
const p = document.createElement('p')
p.innerText = e.data
console.log('Message from server', e.data)
document.querySelector('#log').append(p)
}
setTimeout(function () {
evtSource.close()
}, 5000)
Лучшие практики
No silver bullets: ни одно решение не может быть применено ко всем междоменным сценариям. Рекомендуется использовать соответствующий метод для конкретного сценария.
- Междоменный ресурс
- Страницы общаются друг с другом
- Взаимодействие клиента и сервера
Междоменный ресурс
Для статических ресурсов рекомендуется использовать<link>
<script>
<img>
<iframe>
Собственные возможности тегов позволяют запрашивать ресурсы из разных источников.
Для сторонних интерфейсов рекомендуется реализовать кросс-доменность на основе стандарта CORS.Если браузер не поддерживает CORS, рекомендуется использовать прокси-сервер для кросс-доменности.
Страницы общаются друг с другом
Для связи между страницами мы сначала рекомендуем новый API HTML5 для обмена сообщениями postMessage, который безопасен и удобен.
Во-вторых, когда поддержка браузера плохая, рекомендуется использовать, когда основной домен тот жеdocument.domain
метод, основной домен отличается рекомендуетсяlocation.hash
Способ.
Взаимодействие клиента и сервера
Рекомендуется использовать упрощенный метод SSE для недуплексных сценариев связи.
В сценариях дуплексной связи рекомендуется использовать WebSocket.