1. クロスドメインとは何ですか?
まず、クロスドメインとはブラウザのインターセプトの動作であると述べていますが、リクエストがバックエンドに送信され、バックエンドから返されたレスポンスデータをブラウザがインターセプトする、これがクロスドメインの処理です。
フロントエンド分野では、クロスドメインとは、ブラウザがクロスドメインリクエストをサーバーに送信できるようにすることを意味し、これにより、Ajax は同じオリジンでのみ使用できるという制限が克服されます。
同一生成元ポリシーとは何ですか?
同一オリジン ポリシーは、 1995 年に Netscape によってブラウザに導入された規則です。これはブラウザの中核で最も基本的なセキュリティ機能です。同一オリジン ポリシーがないと、ブラウザは XSS や CSFR などの攻撃に対して脆弱になります。いわゆる相同性とは、「プロトコル + ドメイン名 + ポート」が同じであることを意味します。2 つの異なるドメイン名が同じ IP アドレスを指している場合でも、それらは同じ起源のものではありません。
同一生成元ポリシーは、次の動作を制限します。
- Cookie、LocalStorage、IndexDB を読み取れません
- DOMおよびJSオブジェクトが取得できない
- AJAXリクエストを送信できませんでした
2. 一般的なクロスドメインのシナリオ
URL | 説明する | 通信を許可するかどうか |
---|---|---|
http://www.domain.com/a.js http://www.domain.com/b.js | 同じドメイン名、異なるファイルまたはパス | 許可する |
http://www.domain.com:8000/a.js http://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リクエストを送信すると、サーバーはインターフェイスから返されたデータを結合します。コールバック関数に取り込んでブラウザに返し、解析して実行すると、フロントエンドはコールバック関数によって返されたデータを取得します。
- ネイティブ 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 の欠点: 送信できる get リクエストは 1 種類だけです。
2. クロスオリジンリソース共有 (CORS)
CORSは W3C 標準であり、正式名称は「Cross-Origin Resource Sharing」です。
これにより、ブラウザーが XMLHttpRequest リクエストをクロスオリジン サーバーに送信できるようになり、AJAX が同じオリジンでのみ使用できるという制限が克服されます。
CORS にはブラウザとサーバーの両方のサポートが必要です。現在、すべてのブラウザがこの機能をサポートしており、IE ブラウザは IE10 よりも古いものにすることはできません。
ブラウザーは、CORS クロスドメイン リクエストを単純なリクエストと非単純なリクエストに分割します。
以下の2つの条件を同時に満たしていれば簡単なリクエストです
(1) 次のいずれかの方法を使用します。
- 頭
- 得る
- 役職
(2) 要求されたヘダーは次のとおりです。
- 受け入れる
- 受け入れ言語
- コンテンツ言語
- Content-Type: 3 つの値に制限: application/x-www-form-urlencoded、multipart/form-data、text/plain
上記 2 つの条件が同時に満たされない場合、それは単純ではない要求になります。ブラウザはこれら 2 つを異なる方法で扱います。
簡単なリクエスト
単純なリクエストの場合、ブラウザは 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) Access-Control-Allow-Credentials : オプション。
その値は Cookie の送信を許可するかどうかを示すブール値です。デフォルトでは、Cookie は CORS リクエストに含まれません。true に設定されている場合は、サーバーが Cookie をリクエストに含めてサーバーに一緒に送信できることを明示的に許可していることを意味します。この値は true にのみ設定できます。サーバーがブラウザーに Cookie を送信させたくない場合は、このフィールドを削除してください。
3) Access-Control-Expose-Headers :
CORS リクエストがオプションの場合、XMLHttpRequest オブジェクトの getResponseHeader() メソッドは、Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、プラグマ。他のフィールドを取得したい場合は、Access-Control-Expose-Headers で指定する必要があります。上記の例では、getResponseHeader('FooBar') が FooBar フィールドの値を返すことができることを指定しています。
単純ではないリクエスト
非単純リクエストは、サーバー上で特別な要件を持つリクエストです。たとえば、リクエストメソッドが PUT または DELETE であるか、Content-Type フィールドのタイプが application/json です。単純なリクエストではない CORS リクエストの場合、正式な通信の前に HTTP クエリ リクエストが追加されます。これを「プリフライト」リクエスト (プリフライト) と呼びます。
事前チェックリクエスト
「事前チェック」リクエストに使用されるリクエストメソッドはOPTIONSであり、リクエストが問い合わせに使用されることを示します。リクエストヘッダー情報のキーフィールドは、リクエストの送信元を示すOriginです。 Originフィールド、「事前チェック」リクエスト ヘッダー情報には 2 つの特別なフィールドが含まれます。
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) Access-Control-Request-Method : 必須
ブラウザーの CORS リクエストで使用される HTTP メソッドをリストするために使用されます。上記の例は PUT です。
2) Access-Control-Request-Headers : オプション
このフィールドは、CORS リクエストに対してブラウザによって送信される追加のヘッダー フィールドを指定するカンマ区切りの文字列です。上記の例は X-Custom-Header です。
プリフライト要求への応答
「プリフライト」要求を受信した後、サーバーは、Origin、Access-Control-Request-Method、および Access-Control-Request-Headers フィールドをチェックし、クロスオリジン要求が許可されていることを確認し、応答します。
HTTP 応答では、主要な Access-Control-Allow-Origin フィールドを除き、他の CORS 関連フィールドは次のとおりです。
1) Access-Control-Allow-Methods : 必須
その値はカンマで区切られた文字列で、サーバーがサポートするすべてのクロスドメイン要求メソッドを示します。ブラウザによって要求されたメソッドだけでなく、サポートされているすべてのメソッドが返されることに注意してください。これは、複数の「プリフライト」リクエストを回避するためです。
2) Access-Control-Allow-Headers
ブラウザ要求に Access-Control-Request-Headers フィールドが含まれる場合、Access-Control-Allow-Headers フィールドが必要です。これは、「プリフライト」でブラウザによって要求されたものに限定されず、サーバーによってサポートされるすべてのヘッダー フィールドを示すカンマ区切りの文字列でもあります。
3) Access-Control-Allow-Credentials : オプション
このフィールドは、単純なリクエストの意味と同じ意味を持ちます。
4) Access-Control-Max-Age : オプション
この事前チェック要求の有効期間を秒単位で指定するために使用されます。
CORS クロスドメインの例
1) フロントエンド設定:
ネイティブ ajax:
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) サーバー設定:
Node.jsコード
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 設定はアイコンフォントのクロスドメインを解決します
ブラウザによる js、css、img などの通常の静的リソースへのクロスドメイン アクセスは、同一生成元ポリシーによって許可されていますが、iconfont フォント ファイル (eot|otf|ttf|woff|svg) は例外です。次の構成を nginx 静的リソース サーバーに追加できます。
location / {
add_header Access-Control-Allow-Origin *;
}
2) nginx リバースプロキシインターフェイスクロスドメイン
クロスドメインの問題: 同一生成元ポリシーはブラウザーのみのセキュリティ ポリシーです。サーバーは HTTP プロトコルのみを使用して HTTP インターフェイスを呼び出し、同一生成元ポリシーを必要としないため、クロスドメインの問題は発生しません。
実装アイデア: Nginx を基点として、domain1 と同じドメイン名と異なるポートを持つプロキシ サーバーを構成し、domain2 インターフェイスへのリバース プロキシ アクセスを行い、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 内のドメイン名を変更することもできます現在のドメインの 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 フレームワークのクロスドメイン
ノード + 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. document.domain + iframe クロスドメイン
このソリューションは、同じプライマリ ドメインと異なるサブドメインを使用するクロスドメイン アプリケーション シナリオに限定されます。実現原理: どちらのページも JS を使用して document.domain を基本的なメイン ドメインに強制し、同じドメインが実現されます。
1) 親ウィンドウ: (http://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) 子ウィンドウ: (http://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 を通じて実現します。3 つのページは、iframe の location.hash を使用して異なるドメイン間で値を渡し、同じドメイン間では直接 JS アクセスを通じて相互に通信します。
具体的な実装: ドメイン A: a.html -> ドメイン B: b.html -> ドメイン A: c.html、a と b の異なるドメインはハッシュ値を介して一方向のみ通信でき、異なる b と c のドメインは通信できます。一方向の直接通信のみ可能ですが、c は a と同じドメインにあるため、c はparent.parent を通じてページ a 上のすべてのオブジェクトにアクセスできます。
1)a.html:(http://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)b.html:(http://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:(http://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. window.name + iframe クロスドメイン
window.name 属性の一意性: 名前の値は、異なるページ (異なるドメイン名であっても) を読み込んだ後も存在し、非常に長い名前の値 (2MB) をサポートできます。
1)a.html:(http://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)proxy.html:(http://www.domain1.com/proxy.html)
中間プロキシ ページは a.html と同じドメイン内にあり、コンテンツは空であってもかまいません。
3)b.html:(http://www.domain2.com/b.html)
<script>
window.name = 'This is domain2 data!';
</script>
iframe の src 属性は外部ドメインからローカル ドメインに転送され、クロスドメイン データは iframe の window.name によって外部ドメインからローカル ドメインに転送されます。これはブラウザのクロスドメイン アクセス制限を巧みに回避しますが、同時に安全な操作でもあります。
8.ポストメッセージのクロスドメイン
postMessage は HTML5 XMLHttpRequest レベル 2 の API であり、ドメイン間で操作できる数少ないウィンドウ属性の 1 つであり、次の問題を解決するために使用できます。
- ページと開いた新しいウィンドウのデータ転送
- 複数のウィンドウ間でのメッセージの受け渡し
- ネストされた iframe メッセージを含むページ
- 上記 3 つのシナリオにおけるクロスドメイン データ転送
使用法: postMessage(data,origin) メソッドは 2 つのパラメーターを受け入れます。
- data :
html5 仕様では、あらゆる基本型または再現可能なオブジェクトがサポートされていますが、一部のブラウザーは文字列のみをサポートしているため、パラメーターを渡すときに JSON.stringify() でシリアル化することをお勧めします。 - オリジン: プロトコル + ホスト + ポート番号。「*」に設定することもできます。これは、任意のウィンドウに渡すことができることを意味します。現在のウィンドウと同じオリジンを指定したい場合は、「/」に設定します。
1)a.html:(http://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)b.html:(http://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 の新しいプロトコルです。ブラウザとサーバー間の全二重通信を実現し、同時にクロスドメイン通信も可能にする、サーバープッシュ技術の優れた実装です。
ネイティブの WebSocket API はあまり使いにくいですが、WebSocket インターフェイスを適切にカプセル化し、よりシンプルで柔軟なインターフェイスを提供し、webSocket をサポートしていないブラウザに下位互換性を提供する Socket.io を使用します。
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 リクエストをサポートしますが、ブラウジング Ajax のさまざまなクロスドメイン リクエストに適しています。Nginx プロキシ クロスドメインと Nodejs ミドルウェア クロスドメインの原理は似ています。どちらもサーバーを構築し、サーバー側で HTTP インターフェイスを直接リクエストします。プロジェクトはバックエンド インターフェイスを調整します。document.domain+iframe は、同じメイン ドメイン名と異なるサブドメイン名を持つクロスドメイン リクエストに適しています。postMessage と websocket は両方とも HTML5 の新機能ですが、互換性はあまり良くなく、メインストリーム ブラウザーと IE10 以降にのみ適用できます。