9 распространенных интерфейсных междоменных решений (подробно)

внешний интерфейс

1. Что такое междоменный?

   В поле внешнего интерфейса междоменный означает, что браузер позволяет отправлять междоменные запросы на сервер, тем самым преодолевая только Ajaxгомологияограничения использования.

Что такое политика единого происхождения?

  Та же политика происхожденияЭто соглашение было введено в браузерах компанией Netscape в 1995 г. Это основная и самая основная функция безопасности браузеров.Если политика одинакового происхождения отсутствует, браузеры уязвимы для атак, таких как XSS и CSFR. Так называемая гомология означает, что «протокол + доменное имя + порт» одинаковы, даже если два разных доменных имени указывают на один и тот же IP-адрес, они не гомологичны.

   Политика того же источника ограничивает следующие действия:

  • Файлы cookie, LocalStorage и IndexDB не могут быть прочитаны
  • Объекты DOM и JS недоступны
  • Запрос AJAX не может быть отправлен

2. Распространенные междоменные сценарии

URL иллюстрировать Разрешить ли общение
www.domain.com/a.js
www.domain.com/b.js
www.domain.com/lab/c.js
То же доменное имя, другой файл или путь разрешать
www.domain.com:8000/a.js
www.domain.com/b.js
То же доменное имя, другой порт не положено
http://www.domain.com/a.js
https://www.domain.com/b.js
То же доменное имя, другой протокол не положено
http://www.domain.com/a.js
http://192.168.4.12/b.js
Доменное имя и доменное имя соответствуют одному и тому же ip не положено
http://www.domain.com/a.js
http://x.domain.com/b.js
http://domain.com/c.js
Основной домен тот же, поддомены разные не положено
http://www.domain1.com/a.js
http://www.domain2.com/b.js
разные доменные имена не положено

3. 9 междоменных решений

1. Междоменный JSONP

  jsonpПринцип заключается в использовании<script>Ярлыки не имеют междоменных ограничений, через<script>Пометьте атрибут src, отправьте запрос GET с параметром обратного вызова, сервер соберет воедино данные, возвращенные интерфейсом, в функцию обратного вызова, вернет их в браузер, а браузер проанализирует и выполнит, так что внешний интерфейс получит данные, возвращаемые функцией обратного вызова.
1) Нативная реализация JS:

 <script>
    var script = document.createElement('script');
    script.type = 'text/javascript';

    // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
    script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
    document.head.appendChild(script);

    // 回调执行函数
    function handleCallback(res) {
        alert(JSON.stringify(res));
    }
 </script>

Сервер возвращает следующее (при возврате выполняется глобальная функция):

handleCallback({"success": true, "user": "admin"})

2) реализация jquery Ajax:

$.ajax({
    url: 'http://www.domain2.com:8080/login',
    type: 'get',
    dataType: 'jsonp',  // 请求方式为jsonp
    jsonpCallback: "handleCallback",  // 自定义回调函数名
    data: {}
});

3) Реализация Vue axios:

this.$http = axios;
this.$http.jsonp('http://www.domain2.com:8080/login', {
    params: {},
    jsonp: 'handleCallback'
}).then((res) => {
    console.log(res); 
})

Серверный код node.js:

var querystring = require('querystring');
var http = require('http');
var server = http.createServer();

server.on('request', function(req, res) {
    var params = querystring.parse(req.url.split('?')[1]);
    var fn = params.callback;

    // jsonp返回设置
    res.writeHead(200, { 'Content-Type': 'text/javascript' });
    res.write(fn + '(' + JSON.stringify(params) + ')');

    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

Недостаток jsonp: для получения можно отправить только один вид запроса.

2. Совместное использование ресурсов между источниками (CORS)

  CORSЭто стандарт W3C, полное название — «Совместное использование ресурсов кросс-происхождения» (Cross-origin resource sharing). Это позволяет браузерам отправлять запросы XMLHttpRequest к серверам с разными источниками, тем самым преодолевая ограничение, заключающееся в том, что AJAX можно использовать только с одним и тем же источником. CORS требует поддержки как браузера, так и сервера. В настоящее время все браузеры поддерживают эту функцию, и браузер IE не может быть ниже IE10.
   Браузеры делят междоменные запросы CORS на простые запросы и сложные запросы.
   Если одновременно выполняются следующие два условия, это простой запрос.
(1) Используйте один из следующих методов:

  • head
  • get
  • post

(2) Запрашиваемый хедер

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type: ограничено тремя значениями: application/x-www-form-urlencoded, multipart/form-data, text/plain

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

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

   Для простых запросов браузер напрямую выдает запросы CORS. В частности, к информации заголовка добавляется поле Origin.

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

   В приведенном выше заголовке поле Origin используется для указания источника (протокол + имя домена + порт) этого запроса. На основе этого значения сервер решает, соглашаться ли на запрос.

Все поля заголовка ответа, установленные запросами CORS, начинаются с Access-Control-:

1) Access-Control-Allow-Origin:требуется
   Его значением является либо значение поля Origin на момент запроса, либо *, указывающий, что принимаются запросы от любого доменного имени.
2) Доступ-контроль-разрешение-учетные данные: необязательный
   Его значение является логическим значением, указывающим, разрешать ли отправку файлов cookie. По умолчанию файлы cookie не включаются в запросы CORS. Если установлено значение true, это означает, что сервер явно разрешает включить файл cookie в запрос и отправить его на сервер вместе. Это значение может быть установлено только в true.Если сервер не хочет, чтобы браузер отправлял файлы cookie, просто удалите это поле.
3) Access-Control-Expose-Headers: необязательный
При запросе   CORS метод getResponseHeader() объекта XMLHttpRequest может получить только 6 основных полей: Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma. Если вы хотите получить другие поля, вы должны указать их в Access-Control-Expose-Headers. В приведенном выше примере указано, что getResponseHeader('FooBar') может возвращать значение поля FooBar.

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

  Непростой запрос — это тип запроса, который имеет особые требования к серверу, например метод запроса PUT или DELETE или тип поля Content-Type — application/json. Для запросов CORS, которые не являются простыми запросами, перед формальным общением будет добавлен запрос HTTP-запроса, который называется предварительным запросом.

предварительный запрос

Метод запроса, используемый для «предварительного» запроса, – OPTIONS, что означает, что запрос используется для запроса. В заголовке запроса ключевое поле – Origin, указывающее, из какого источника поступил запрос. В дополнение к полю Origin, информация заголовка «предпечатного» запроса включает в себя два специальных поля.

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0..

1) Метод запроса-управления доступом:требуется
   используется для указания того, какие методы HTTP будут использоваться браузером для запросов CORS.Приведенный выше пример — PUT.
2) Заголовки запроса управления доступом: необязательный
   Это поле представляет собой строку, разделенную запятыми, которая указывает дополнительные поля заголовка, которые будут отправляться запросами CORS браузера. Пример выше — X-Custom-Header.

Ответ на предварительную заявку

   После того, как сервер получает «предварительный» запрос, он проверяет поля Origin, Access-Control-Request-Method и Access-Control-Request-Headers, подтверждает, что запросы между источниками разрешены, и может отвечать.
В ответе   HTTP помимо ключевого поля Access-Control-Allow-Origin есть и другие поля, связанные с CORS:
1) Методы контроля доступа:требуется
   Его значение представляет собой строку, разделенную запятыми, указывающую все методы, поддерживаемые сервером для междоменных запросов. Обратите внимание, что возвращаются все поддерживаемые методы, а не только запрошенный браузером. Это сделано для того, чтобы избежать множественных "предполетных" запросов.
2) Access-Control-Allow-Headers
   Поле Access-Control-Allow-Headers является обязательным, если запрос браузера включает поле Access-Control-Request-Headers. Это также строка с разделителями-запятыми, указывающая все поля заголовка, поддерживаемые сервером, не ограничиваясь теми, которые запрошены браузером в «предпечатной проверке».
3) Доступ-контроль-разрешение-учетные данные: необязательный
   Это поле имеет то же значение, что и в простом запросе.
4) Access-Control-Max-Age: необязательный
   используется для указания периода действия этого запроса предварительной проверки в секундах.

Междоменный пример CORS

1) Настройки внешнего интерфейса:

  • Родной аякс:
var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容

// 前端设置是否带cookie
xhr.withCredentials = true;

xhr.open('post', 'http://www.domain2.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');

xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        alert(xhr.responseText);
    }
};
  • jquery ajax:
$.ajax({
    ...
   xhrFields: {
       withCredentials: true    // 前端设置是否带cookie
   },
   crossDomain: true,   // 会让请求头中包含跨域的额外信息,但不会含cookie
    ...
});

2) Настройки сервера:

  • Nodejs код
var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
    var postData = '';

    // 数据块接收中
    req.addListener('data', function(chunk) {
        postData += chunk;
    });

    // 数据接收完毕
    req.addListener('end', function() {
        postData = qs.parse(postData);

        // 跨域后台设置
        res.writeHead(200, {
            'Access-Control-Allow-Credentials': 'true',     // 后端允许发送Cookie
            'Access-Control-Allow-Origin': 'http://www.domain1.com',    // 允许访问的域(协议+域名+端口)
            /* 
             * 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
             * 但只要domain2中写入一次cookie认证,后面的跨域接口都能从domain2中获取cookie,从而实现所有的接口都能跨域访问
             */
            'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'  // HttpOnly的作用是让js无法读取cookie
        });

        res.write(JSON.stringify(postData));
        res.end();
    });
});

server.listen('8080');
console.log('Server is running at port 8080...');

3, междоменный прокси nginx

  Кроссдоменный прокси-сервер nginx, по сути такой же, как кросс-доменный принцип CORS, задайте заголовок ответа на запрос Access-Control-Allow-Origin... и другие поля через файл конфигурации.

1) Конфигурация nginx решает вопрос междоменного использования iconfont

Междоменный доступ браузеров к обычным статическим ресурсам, таким как js, css и img, разрешен политикой одного источника, за исключением файлов шрифтов iconfont (eot|otf|ttf|woff|svg). Конфигурация может быть добавлена ​​к серверу статических ресурсов nginx.

location / {
  add_header Access-Control-Allow-Origin *;
}

2) междоменный интерфейс обратного прокси nginx

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

Идея реализации: настроить прокси-сервер с тем же именем домена, что и у домена1, но другими портами через Nginx в качестве трамплина, обратный прокси-доступ к интерфейсу домена2, и вы можете изменить информацию о домене в файле cookie, чтобы облегчить запись текущий файл cookie домена и обеспечить междоменный доступ.
Конкретная конфигурация 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;
    }
}

4. Междоменный прокси промежуточного ПО Nodejs

Промежуточное ПО ноды реализует междоменный прокси.Принцип примерно такой же, как и у nginx.Пересылка данных осуществляется путем запуска прокси-сервера.Также можно изменить доменное имя в куки в заголовке ответа, установив для параметра cookieDomainRewrite значение реализовать запись cookie текущего домена, что удобно для аутентификации входа в интерфейс.
1) Междоменный фреймворк, отличный от Vue
   Используйте node + express + http-proxy-middleware для создания прокси-сервера.

  • Интерфейсный код:
var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();
  • Код сервера промежуточного слоя:
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();

app.use('/', proxy({
    // 代理跨域目标接口
    target: 'http://www.domain2.com:8080',
    changeOrigin: true,

    // 修改响应头信息,实现跨域并允许带cookie
    onProxyRes: function(proxyRes, req, res) {
        res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
        res.header('Access-Control-Allow-Credentials', 'true');
    },

    // 修改响应信息中的cookie域名
    cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
}));

app.listen(3000);
console.log('Proxy server is listen at port 3000...');

2) Кросс-домен vue framework
Проект, созданный с помощью   node + vue + webpack + webpack-dev-server, интерфейс междоменных запросов, напрямую изменяет конфигурацию webpack.config.js. В среде разработки служба рендеринга vue и прокси-служба интерфейса — это один и тот же webpack-dev-server, поэтому страница и прокси-интерфейс больше не являются междоменными.
Частичная конфигурация webpack.config.js:

module.exports = {
    entry: {},
    module: {},
    ...
    devServer: {
        historyApiFallback: true,
        proxy: [{
            context: '/login',
            target: 'http://www.domain2.com:8080',  // 代理跨域目标接口
            changeOrigin: true,
            secure: false,  // 当代理某些https服务报错时用
            cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
        }],
        noInfo: true
    }
}

5. междоменный документ.домен + iframe

  Это решение ограничено сценариями междоменных приложений с одним и тем же основным доменом и разными поддоменами. Принцип реализации: Обе страницы через js принудительно устанавливают document.domain как базовый основной домен, и реализуется один и тот же домен.
1) Родительское окно :(www.domain.com/a.html)

<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
    document.domain = 'domain.com';
    var user = 'admin';
</script>

1) Детское окно :(child.domain.com/a.html)

<script>
    document.domain = 'domain.com';
    // 获取父窗口中变量
    console.log('get js data from parent ---> ' + window.parent.user);
</script>

6. перекрестный домен location.hash + iframe

   Принцип реализации: a хочет общаться с b через домены и реализует это через промежуточную страницу c. Для трех страниц для передачи значений между разными доменами используется location.hash iframe, а для связи между одними и теми же доменами используется прямой js-доступ.
Конкретная реализация: Домен A: a.html -> Домен B: b.html -> Домен A: c.html, разные домены a и b могут обмениваться данными только в одностороннем порядке через хеш-значение, а b и c также являются разными доменами. может быть только однонаправленным Для связи, но c находится в том же домене, что и a, поэтому c может получить доступ ко всем объектам на странице через parent.parent.
1) а.html :(www.domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 向b.html传hash值
    setTimeout(function() {
        iframe.src = iframe.src + '#user=admin';
    }, 1000);
    
    // 开放给同域c.html的回调方法
    function onCallback(res) {
        alert('data from c.html ---> ' + res);
    }
</script>

2) б.html :(www.domain2.com/b.html)

<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 监听a.html传来的hash值,再传给c.html
    window.onhashchange = function () {
        iframe.src = iframe.src + location.hash;
    };
</script>

3) c.html :(www.domain1.com/c.html)

<script>
    // 监听b.html传来的hash值
    window.onhashchange = function () {
        // 再通过操作同域a.html的js回调,将结果传回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
</script>

7, междоменное имя окна + iframe

Уникальность свойства   window.name: значение имени сохраняется после загрузки разных страниц (даже разных доменных имен) и может поддерживать очень длинные значения имени (2 МБ).
1) а.html :(www.domain1.com/a.html)

var proxy = function(url, callback) {
    var state = 0;
    var iframe = document.createElement('iframe');

    // 加载跨域页面
    iframe.src = url;

    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
    iframe.onload = function() {
        if (state === 1) {
            // 第2次onload(同域proxy页)成功后,读取同域window.name中数据
            callback(iframe.contentWindow.name);
            destoryFrame();

        } else if (state === 0) {
            // 第1次onload(跨域页)成功后,切换到同域代理页面
            iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
            state = 1;
        }
    };

    document.body.appendChild(iframe);

    // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
    function destoryFrame() {
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
    }
};

// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
    alert(data);
});

2) прокси.html :(www.domain1.com/proxy.html)
  Промежуточная прокси-страница, тот же домен, что и .html, содержимое может быть пустым.

3) б.html :(www.domain2.com/b.html)

<script>
    window.name = 'This is domain2 data!';
</script>

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

8. postMessage междоменный

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

  • Передача данных страницы и новое окно, которое она открывает
  • Передача сообщений между несколькими окнами
  • Страницы с вложенными сообщениями iframe
  • Междоменная передача данных в описанных выше трех сценариях

Использование: метод postMessage(data, origin) принимает два параметра:

  • data: Спецификация html5 поддерживает любой базовый тип или копируемый объект, но некоторые браузеры поддерживают только строки, поэтому лучше всего использовать JSON.stringify() для сериализации при передаче параметров.
  • origin: Протокол + хост + номер порта, также может быть установлен в "*", что означает, что он может быть передан в любое окно. Если вы хотите указать тот же источник, что и текущее окно, установите его в "/".

1) а.html :(www.domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>       
    var iframe = document.getElementById('iframe');
    iframe.onload = function() {
        var data = {
            name: 'aym'
        };
        // 向domain2传送跨域数据
        iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
    };

    // 接受domain2返回数据
    window.addEventListener('message', function(e) {
        alert('data from domain2 ---> ' + e.data);
    }, false);
</script>

2) б.html :(www.domain2.com/b.html)

<script>
    // 接收domain1的数据
    window.addEventListener('message', function(e) {
        alert('data from domain1 ---> ' + e.data);

        var data = JSON.parse(e.data);
        if (data) {
            data.number = 16;

            // 处理后再发回domain1
            window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
        }
    }, false);
</script>

9. Междоменный протокол WebSocket

  Протокол WebSocket — это новый протокол HTML5. Он реализует полнодуплексную связь между браузером и сервером и позволяет осуществлять междоменную связь, что является хорошей реализацией технологии push-уведомлений сервера. Нативный WebSocket API неудобен в использовании, мы используем Socket.io, который хорошо инкапсулирует интерфейс webSocket, предоставляет более простой и гибкий интерфейс и обеспечивает обратную совместимость для браузеров, не поддерживающих webSocket.
1) Интерфейсный код:

<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');

// 连接成功处理
socket.on('connect', function() {
    // 监听服务端消息
    socket.on('message', function(msg) {
        console.log('data from server: ---> ' + msg); 
    });

    // 监听服务端关闭
    socket.on('disconnect', function() { 
        console.log('Server socket has closed.'); 
    });
});

document.getElementsByTagName('input')[0].onblur = function() {
    socket.send(this.value);
};
</script>

2) Фон сокета Nodejs:

var http = require('http');
var socket = require('socket.io');

// 启http服务
var server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-type': 'text/html'
    });
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

// 监听socket连接
socket.listen(server).on('connection', function(client) {
    // 接收信息
    client.on('message', function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
    });

    // 断开处理
    client.on('disconnect', function() {
        console.log('Client socket has closed.'); 
    });
});

резюме

Выше приведены 9 распространенных междоменных решений, jsonp (поддерживает только запросы get, поддерживает старые браузеры IE) подходит для загрузки js, css, img и других статических ресурсов разных доменных имен; CORS (поддерживает все типы HTTP-запросов, но просмотр сервера IE10 или ниже не поддерживается) подходит для различных междоменных запросов ajax; принципы междоменного междоменного прокси-сервера Nginx и междоменного промежуточного программного обеспечения nodejs аналогичны, как построение сервера, так и запрос HTTP-интерфейса непосредственно на стороне сервера, что подходит для разделения front-end и back-end.В проекте настраивается back-end интерфейс. document.domain+iframe подходит для междоменных запросов с одним и тем же основным доменным именем и разными поддоменами. postMessage и websocket — новые функции HTML5, совместимость не очень хорошая, применима только к основным браузерам и IE10+.
   При таком количестве междоменных решений нет лучшего, есть только самое подходящее.Выберите междоменное решение в соответствии с конкретными сценариями использования. Я надеюсь, что этот пост в блоге поможет вам~~