предисловие
Взаимодействия с внешними и внутренними данными часто сталкиваются с междоменными запросами, что такое междоменные методы и какие виды междоменных методов существуют.Это содержание этой статьи.
Щелкните полный исходный код этой статьи.блог на гитхабе, Это мелко на бумаге, я предлагаю вам начать вводить код.
1. Что такое междоменный?
1. Что такое политика единого происхождения и что она ограничивает?
Политика одинакового происхождения является соглашением. Это основная и самая основная функция безопасности браузера. Без политики того же происхождения браузер уязвим для атак, таких как XSS и CSRF. Так называемая гомология означает, что «протокол + доменное имя + порт» одинаковы, даже если два разных доменных имени указывают на один и тот же IP-адрес, они не гомологичны.
Ограничения политики того же происхождения включают:- Содержимое хранилища, такое как файлы cookie, LocalStorage, IndexedDB.
- DOM-узел
- После отправки AJAX-запроса результат перехватывается браузером
Но есть три тега, которые разрешают загрузку ресурсов из разных источников:
<img src=XXX>
<link href=XXX>
<script src=XXX>
2. Распространенный междоменный сценарий
Если какой-либо протокол, имя поддомена, имя основного домена и номер порта отличаются, они считаются разными доменами.. Запрос ресурсов друг у друга между разными доменами считается «междоменным». Общие междоменные сценарии показаны на следующем рисунке:
Особенно два момента:
Во-первых: если это междоменная проблема, вызванная протоколами и портами, «передний план» бессилен.
Во-вторых: в междоменной проблеме это определяется только «заголовком URL-адреса», а не на основе того, совпадают ли IP-адреса, соответствующие доменному имени. «Заголовок URL» можно понимать как «протокол, имя домена и порт должны совпадать»..
Здесь у вас может возникнуть вопрос:Запрос является междоменным, поэтому запрос отправлен?
Кроссдоменность не означает, что запрос нельзя отправить, запрос можно отправить, сервер может принять запрос и вернуть результат нормально, но результат перехватывается браузером. Вы можете задаться вопросом, почему Ajax не может сделать междоменный запрос через форму Safe, поэтому ответ перехватывается. Но форма не получает нового содержимого, поэтому можно делать междоменные запросы. Это также показывает, что кросс-доменность не может полностью предотвратить CSRF, потому что запрос все-таки отправляется.
2. Междоменные решения
1.jsonp
1) Принцип JSONP
использовать<script>
Теги не имеют междоменных ограничений, и веб-страницы могут получать данные JSON, динамически генерируемые из других источников. Запросы JSONP должны поддерживаться сервером другой стороны.
2) Сравнение JSONP и AJAX
JSONP аналогичен AJAX, и оба они представляют собой способ, которым клиент отправляет запрос на сервер и получает данные с сервера. Но AJAX относится к политике одного и того же происхождения, JSONP относится к политике другого происхождения (междоменный запрос).
3) Преимущества и недостатки JSONP
Преимущество JSONP заключается в том, что он прост и совместим, и его можно использовать для решения проблемы междоменного доступа к данным в основных браузерах.Недостатком является то, что только поддержка метода get имеет ограничения, небезопасна и может страдать от XSS-атак.
4) Процесс внедрения JSONP
- Объявите функцию обратного вызова, имя функции которой (например, show) используется в качестве значения параметра, передаваемого на сервер, который запрашивает данные между доменами, а параметр функции — это целевые данные, которые необходимо получить (данные, возвращаемые сервером).
- Создавать
<script>
Пометить, присвоить адрес междоменного API-интерфейса данных src скрипта и в этом адресе передать серверу имя функции (можно передать параметр через вопросительный знак: ?callback=show). - После того, как сервер получит запрос, ему необходимо выполнить специальную обработку: объединить переданное имя функции и данные, которые он должен вам предоставить, в строку, например: переданное имя функции — show, а ее подготовленные данные —
show('我不爱你')
. - Наконец, сервер возвращает подготовленные данные клиенту через протокол HTTP, а затем клиент вызывает и выполняет функцию обратного вызова (show), объявленную перед операцией с возвращенными данными.
При разработке может возникнуть ситуация, когда имена callback-функций нескольких JSONP-запросов совпадают, в этом случае вам нужно самостоятельно инкапсулировать JSONP-функцию.
// index.html
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 = { ...params, callback } // wd=b&callback=show
let arrs = []
for (let key in params) {
arrs.push(`${key}=${params[key]}`)
}
script.src = `${url}?${arrs.join('&')}`
document.body.appendChild(script)
})
}
jsonp({
url: 'http://localhost:3000/say',
params: { wd: 'Iloveyou' },
callback: 'show'
}).then(data => {
console.log(data)
})
Приведенный выше код эквивалентенhttp://localhost:3000/say?wd=Iloveyou&callback=show
Этот адрес запрашивает данные, а затем вернуться назадshow('我不爱你')
, и, наконец, запустите функцию show(), которая напечатает «Я не люблю тебя».
// server.js
let express = require('express')
let app = express()
app.get('/say', function(req, res) {
let { wd, callback } = req.query
console.log(wd) // Iloveyou
console.log(callback) // show
res.end(`${callback}('我不爱你')`)
})
app.listen(3000)
5) jsonp-форма jQuery
JSONP — это все GET и асинхронные запросы, других методов запросов и синхронных запросов нет, и jQuery по умолчанию очистит кеш для запросов JSONP.
$.ajax({
url:"http://crossdomain.com/jsonServerResponse",
dataType:"jsonp",
type:"get",//可以省略
jsonpCallback:"show",//->自定义传递给服务器的函数名,而不是使用jQuery自动生成的,可省略
jsonp:"callback",//->把传递函数名的那个形参callback,可省略
success:function (data){
console.log(data);}
});
2.cors
CORS требует поддержки как браузера, так и серверной части. IE 8 и 9 должны делать это через XDomainRequest.
Браузер будет автоматически осуществлять связь CORS, а ключом к реализации связи CORS является серверная часть. Пока серверная часть реализует CORS, реализуется междоменный доступ.
Сервер может установить Access-Control-Allow-Origin для включения CORS. Этот атрибут указывает, какие доменные имена могут получить доступ к ресурсу. Если установлен подстановочный знак, это означает, что все веб-сайты могут получить доступ к ресурсу.
Хотя настройка CORS не имеет ничего общего с фронтендом, но если решить междоменную проблему таким образом, то при отправке запросов будет две ситуации, а именнопростой запроса такжесложный запрос.
1) Простой запрос
Пока следующие два условия выполняются одновременно, это простой запрос
Условие 1. Используйте один из следующих методов:
- GET
- HEAD
- POST
Условие 2: значение Content-Type ограничено одним из следующих трех:
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
Для каких-либо объектов XMLHttpRequestUpload в запросе не регистрируются прослушиватели событий; доступ к объектам XMLHttpRequestUpload можно получить с помощью свойства XMLHttpRequest.upload.
2) сложный запрос
Запрос, который не соответствует вышеуказанным условиям, определенно является сложным запросом. Для запросов CORS сложных запросов перед формальным общением будет добавлен запрос HTTP-запроса, который называется предварительным запросом. Этот запрос является дополнительным методом, и этот запрос используется, чтобы узнать, разрешает ли сервер междоменные запросы.
мы используемPUT
При запросе к фону это сложный запрос, и фон необходимо настроить следующим образом:
// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// OPTIONS请求不做任何处理
if (req.method === 'OPTIONS') {
res.end()
}
// 定义后台返回的内容
app.put('/getData', function(req, res) {
console.log(req.headers)
res.end('我不爱你')
})
Далее давайте рассмотрим пример полного сложного запроса и представим поля, связанные с запросом CORS.
// index.html
let xhr = new XMLHttpRequest()
document.cookie = 'name=xiamen' // cookie不能跨域
xhr.withCredentials = true // 前端设置是否带cookie
xhr.open('PUT', 'http://localhost:4000/getData', true)
xhr.setRequestHeader('name', 'xiamen')
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
console.log(xhr.response)
//得到响应头,后台需设置Access-Control-Expose-Headers
console.log(xhr.getResponseHeader('name'))
}
}
}
xhr.send()
//server1.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000);
//server2.js
let express = require('express')
let app = express()
let whitList = ['http://localhost:3000'] //设置白名单
app.use(function(req, res, next) {
let origin = req.headers.origin
if (whitList.includes(origin)) {
// 设置哪个源可以访问我
res.setHeader('Access-Control-Allow-Origin', origin)
// 允许携带哪个头访问我
res.setHeader('Access-Control-Allow-Headers', 'name')
// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 允许携带cookie
res.setHeader('Access-Control-Allow-Credentials', true)
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// 允许返回的头
res.setHeader('Access-Control-Expose-Headers', 'name')
if (req.method === 'OPTIONS') {
res.end() // OPTIONS请求不做任何处理
}
}
next()
})
app.put('/getData', function(req, res) {
console.log(req.headers)
res.setHeader('name', 'jw') //返回一个响应头,后台需设置
res.end('我不爱你')
})
app.get('/getData', function(req, res) {
console.log(req.headers)
res.end('我不爱你')
})
app.use(express.static(__dirname))
app.listen(4000)
Приведенный выше код даетсяhttp://localhost:3000/index.html
Кhttp://localhost:4000/
Запросы между источниками, как мы сказали выше, серверная часть является ключом к связи CORS.
3.postMessage
postMessage — это API в HTML5 XMLHttpRequest Level 2 и один из немногих атрибутов окна, которые могут работать между доменами. Его можно использовать для решения следующих проблем:
- Передача данных страницы и новое окно, которое она открывает
- Передача сообщений между несколькими окнами
- Страницы с вложенными сообщениями iframe
- Междоменная передача данных в описанных выше трех сценариях
Метод postMessage() позволяет сценариям из разных источников обмениваться данными асинхронно с ограниченным взаимодействием, обеспечивая межтекстовый документ, многооконный и междоменный обмен сообщениями..
otherWindow.postMessage(message, targetOrigin, [transfer]);
- message: Данные для отправки в другие окна.
- targetOrigin: укажите, какие окна могут получать события сообщения через свойство origin окна, и его значение может быть строкой "*" (представляющей неограниченное количество) или URI. При отправке сообщения, если какой-либо протокол, адрес хоста или порт целевого окна не соответствует значению, предоставленному targetOrigin, сообщение не будет отправлено; сообщение будет отправлено только в том случае, если они полностью совпадают.
- передача (необязательно): это строка объектов Transferable, передаваемых одновременно с сообщением. Право собственности на эти объекты будет передано получателю сообщения, а отправитель больше не сохранит право собственности.
Далее рассмотрим пример:http://localhost:3000/a.html
страница вhttp://localhost:4000/b.html
Проходит «Я тебя люблю», которое затем возвращается «Я тебя не люблю».
// a.html
<iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe> //等它加载完触发一个事件
//内嵌在http://localhost:3000/a.html
<script>
function load() {
let frame = document.getElementById('frame')
frame.contentWindow.postMessage('我爱你', 'http://localhost:4000') //发送数据
window.onmessage = function(e) { //接受返回数据
console.log(e.data) //我不爱你
}
}
</script>
// b.html
window.onmessage = function(e) {
console.log(e.data) //我爱你
e.source.postMessage('我不爱你', e.origin)
}
4.websocket
Websocket — это постоянный протокол HTML5, который реализует полнодуплексную связь между браузером и сервером, а также является междоменным решением. И WebSocket, и HTTP являются протоколами прикладного уровня, оба основаны на протоколе TCP. ноWebSocket — это протокол двусторонней связи.После установления соединения как сервер WebSocket, так и клиент могут активно отправлять или получать данные друг другу.. В то же время WebSocket необходимо использовать протокол HTTP при установлении соединения, после установления соединения двусторонняя связь между клиентом и сервером не имеет ничего общего с HTTP.
Родной WebSocket API не очень удобен в использовании, мы используемSocket.io
, который хорошо инкапсулирует интерфейс webSocket, предоставляет более простой и гибкий интерфейс и обеспечивает обратную совместимость для браузеров, не поддерживающих webSocket.
Сначала рассмотрим пример: локальный файл socket.html.localhost:3000
Данные о вхождении и получение данных
// socket.html
<script>
let socket = new WebSocket('ws://localhost:3000');
socket.onopen = function () {
socket.send('我爱你');//向服务器发送数据
}
socket.onmessage = function (e) {
console.log(e.data);//接收服务器返回的数据
}
</script>
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');//记得安装ws
let wss = new WebSocket.Server({port:3000});
wss.on('connection',function(ws) {
ws.on('message', function (data) {
console.log(data);
ws.send('我不爱你')
});
})
5. Прокси промежуточного программного обеспечения узла (дважды междоменный)
Принцип реализации:Политика одного и того же источника является стандартом, которому должны следовать браузеры, и если это межсерверный запрос, ему не нужно следовать политике одного и того же источника.Прокси-сервер требует следующих шагов:
- Принимать запросы клиентов.
- Перенаправить запрос на сервер.
- Получите данные ответа сервера.
- Направить ответ клиенту.
Сначала рассмотрим пример: локальный файл index.html через прокси-серверhttp://localhost:3000
на целевой серверhttp://localhost:4000
Запросить данные.
// index.html(http://127.0.0.1:5500)
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script>
$.ajax({
url: 'http://localhost:3000',
type: 'post',
data: { name: 'xiamen', password: '123456' },
contentType: 'application/json;charset=utf-8',
success: function(result) {
console.log(result) // {"title":"fontend","password":"123456"}
},
error: function(msg) {
console.log(msg)
}
})
</script>
// server1.js 代理服务器(http://localhost:3000)
const http = require('http')
// 第一步:接受客户端请求
const server = http.createServer((request, response) => {
// 代理服务器,直接和浏览器直接交互,需要设置CORS 的首部字段
response.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': 'Content-Type'
})
// 第二步:将请求转发给服务器
const proxyRequest = http
.request(
{
host: '127.0.0.1',
port: 4000,
url: '/',
method: request.method,
headers: request.headers
},
serverResponse => {
// 第三步:收到服务器的响应
var body = ''
serverResponse.on('data', chunk => {
body += chunk
})
serverResponse.on('end', () => {
console.log('The data is ' + body)
// 第四步:将响应结果转发给浏览器
response.end(body)
})
}
)
.end()
})
server.listen(3000, () => {
console.log('The proxyServer is running at http://localhost:3000')
})
// server2.js(http://localhost:4000)
const http = require('http')
const data = { title: 'fontend', password: '123456' }
const server = http.createServer((request, response) => {
if (request.url === '/') {
response.end(JSON.stringify(data))
}
})
server.listen(4000, () => {
console.log('The server is running at http://localhost:4000')
})
Вышеприведенный код проходит через два кросс-домена.Стоит отметить, что браузер отправляет запрос на прокси-сервер, а также следует политике того же происхождения и, наконец, выводит его в файл index.html.{"title":"fontend","password":"123456"}
6. Обратный прокси nginx
Принцип реализации аналогичен прокси Node промежуточного ПО, который требует создания транзитного сервера nginx для пересылки запросов.
Использование обратного прокси-сервера nginx для достижения междоменного доступа — это самый простой способ междоменного взаимодействия. Требуется только изменить конфигурацию nginx для решения междоменных проблем, поддерживает все браузеры, поддерживает сеансы, не требует изменения кода и не влияет на производительность сервера.
Идея реализации: настроить прокси-сервер (имя домена такое же, как у домена1, но другие порты) через nginx в качестве трамплина, обратный прокси для доступа к интерфейсу домена2, и вы можете изменить информацию о домене в файле cookie кстати, который удобен для записи текущего доменного файла cookie и реализации междоменного входа в систему.
скачать первымnginx, а затем измените nginx.conf в каталоге nginx следующим образом:
// proxy服务器
server {
listen 81;
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 -s reload
запустить 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=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly:脚本无法读取
});
res.write(JSON.stringify(params));
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
7.window.name + iframe
Уникальность свойства window.name: значение имени сохраняется после загрузки разных страниц (даже разных доменных имен) и может поддерживать очень длинные значения имени (2 МБ).
Если a.html и b.html находятся в одном домене, обаhttp://localhost:3000
; в то время как c.htmlhttp://localhost:4000
// a.html(http://localhost:3000/b.html)
<iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
<script>
let first = true
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
function load() {
if(first){
// 第1次onload(跨域页)成功后,切换到同域代理页面
let iframe = document.getElementById('iframe');
iframe.src = 'http://localhost:3000/b.html';
first = false;
}else{
// 第2次onload(同域b.html页)成功后,读取同域window.name中数据
console.log(iframe.contentWindow.name);
}
}
</script>
b.html — это промежуточная прокси-страница, тот же домен, что и a.html, и содержимое пустое.
// c.html(http://localhost:4000/c.html)
<script>
window.name = '我不爱你'
</script>
Резюме: через атрибут src iframe из чужого домена в локальный домен междоменные данные передаются из чужого домена в локальный домен с помощью окна.имя iframe. Это ловко обходит ограничения междоменного доступа браузера, но в то же время это безопасная операция.
8.location.hash + iframe
Принцип реализации: a.html хочет общаться с c.html через домены и реализует это через промежуточную страницу b.html. Для трех страниц для передачи значений между разными доменами используется location.hash iframe, а для связи между одними и теми же доменами используется прямой js-доступ.
Конкретные этапы реализации: сначала a.html передает хеш-значение в c.html, затем c.html получает хеш-значение, затем передает хэш-значение в b.html и, наконец, b.html помещает результат в a.html в хэш-значение.
Точно так же a.html и b.html находятся в одном домене, обаhttp://localhost:3000
; в то время как c.htmlhttp://localhost:4000
// a.html
<iframe src="http://localhost:4000/c.html#iloveyou"></iframe>
<script>
window.onhashchange = function () { //检测hash的变化
console.log(location.hash);
}
</script>
// b.html
<script>
window.parent.parent.location.hash = location.hash
//b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面
</script>
// c.html
console.log(location.hash);
let iframe = document.createElement('iframe');
iframe.src = 'http://localhost:3000/b.html#idontloveyou';
document.body.appendChild(iframe);
9.document.domain + iframe
Этот метод можно использовать только в том случае, если доменные имена второго уровня совпадают, напримерa.test.com
а такжеb.test.com
Применяется к этому методу.
Просто добавьте на страницуdocument.domain ='test.com'
Указывает, что доменные имена второго уровня одинаковы для обеспечения междоменного доступа.
Принцип реализации: Обе страницы через js принудительно устанавливают document.domain как базовый основной домен, и реализуется один и тот же домен.
Рассмотрим пример: страницаa.zf1.cn:3000/a.html
получить страницуb.zf1.cn:3000/b.html
значение в
// a.html
<body>
helloa
<iframe src="http://b.zf1.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>
<script>
document.domain = 'zf1.cn'
function load() {
console.log(frame.contentWindow.a);
}
</script>
</body>
// b.html
<body>
hellob
<script>
document.domain = 'zf1.cn'
var a = 100;
</script>
</body>
3. Резюме
- CORS поддерживает все типы HTTP-запросов, что является фундаментальным решением для перекрестных HTTP-запросов.
- JSONP поддерживает только получение запросов. Преимущество JSONP заключается в том, что он поддерживает старые браузеры и может запросить данные с веб-сайтов, которые не поддерживают CORS.
- Будь то прокси промежуточного слоя Node или обратный прокси nginx, сервер не ограничен политикой одного и того же источника.
- В повседневной работе чаще всего используются междоменные решения — cors и обратные прокси nginx.
Порекомендуйте полезный инструмент мониторинга ошибок для всехFundebug, добро пожаловать, чтобы попробовать это бесплатно!
Справочная статья
- Архитектурный класс Everest (настоятельно рекомендуется)
- Подробное объяснение CORS для совместного использования ресурсов между доменами
- предварительное интервью
- window.postMessage
- Стандартные междоменные решения для внешнего интерфейса (полное)
- Углубленные междоменные проблемы (4) - Использование прокси для решения междоменных проблем