跨域的定义
- 同源策略:是浏览器避免浏览器收到XSS、CSRF等恶意攻击的一种安全策略,指的是两个url具有完全相同的协议、域名及端口号;
- 一般我们所说的跨域是指访问非同源的资源的行为;浏览器通常会允许发送跨域请求,但是会拦截跨域请求的响应;这主要是为了防止恶意网站读取其他网站的可信任信息,以及合理读取其他网站的网页内容;
- IE中同源策略的特殊性:a、在IE中,如果两个域名在相同的高度信任区域(如企业的内网),那么同源策略不适用;b、IE的同源策略检测会忽略端口号。
- 通常,嵌入式的跨域资源请求方式是被允许的,这类请求包括:
<script src="..."></script>
<link rel="stylesheet" href="">
<img src="...">
video
及audio
标签@font-face()
:该方法在不同的浏览器存在一定的差异,部分浏览器仍然有同源策略限制<frame>
及<iframe>
跨域的方法
JSONP
- 原理: JSONP跨域的原理是
script
标签请求的资源不受同源策略的限制;JSONP跨域是比较常见的,但JSONP跨域需要服务器端相应的处理才能支持;
通常可以动态创建一个script
标签,然后在请求的url中带上一些处理参数,一般是一个回调函数;服务端在接收到请求后,从请求中获得处理参数,然后将处理结果返回给客户端;如果返回的是函数调用,那么客户端的回调函数就会被调用执行; - 限制:JSONP只支持
GET
请求方式,本质上是因为script
标签请求资源的方式就是GET
;另外,根据实现过程可以看到,jsonp的返回是一个函数调用,容易导致XSS攻击等安全问题; - 实现
客户端代码如下,其URL为http://localhost:8001/test/test1.html
;请求的服务器地址为:http://localhost:8080/
;为了跨域,在请求的url加入一个callback参数指定jsonp回调函数名称;服务端接收到请求后从中提取请求参数及回调函数名,然后将对应的回调函数包裹相应参数返回给客户端handleCallback(id)
;这时客户端的handleCallback()
函数就会被自动调用。<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Page Title</title> <meta name="viewport" content="width=device-width, initial-scale=1"> </head> <body> <div class="hook"></div> <script> var script = document.createElement('script'); script.type = 'text/javascript'; script.src = 'http://localhost:8080/?callback=handleCallback'; document.head.appendChild(script); // jquery请求方式 function ajax() { $.ajax({ url: 'http://localhost:8080', type: 'GET', dataType: 'jsonp', jsonpCallback: 'handleCallback', }) } function handleCallback(res) { document.querySelector('.hook').innerHTML = `请求次数: ${res}`; } </script> </body> </html>
var qs = require('querystring'); var http = require('http'); var server = http.createServer(); var id = 0; server.on('request', function (req, res) { console.log(req.url); console.log(req.url.split('?')); var params = qs.parse(req.url.split('?')[1]); var fn = params.callback; res.writeHead(200, {'Content-Type': 'text/javascript'}); res.write(fn + '(' + id++ + ')'); res.end(); }) server.listen('8080'); console.log('server is running at port 8080...');
CORS
-
定义:CORS(Cross-ORigin Resource Sharing),即跨站资源共享机制,通过在服务端和客户端添加一定的标头进行跨域访问控制,;
XHR
及Fetch
等API均支持CORS。跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。
在使用CORS时,如果请求的方法可能修改或影响服务端数据,那么浏览器通常会发送一个预检请求
Option
,以确认服务器是否允许跨域,并且在允许跨域时,告知浏览器是否要携带相应的身份认证信息(cookie、token等);预检请求确认可以进行跨域请求时才会发起真正的请求; -
适用情形:
XHR
或Fetch
请求;@font-face
加载跨域字体;- canvas中使用
drawImage
绘制audio
或video
;
-
简单请求:
前面已经提到,触发预检请求的请求方式只有一部分,通常将不会触发CORS预检请求的称为简单请求,通常满足以下所有条件:- 使用
GET
、POST
或HEAD
请求; Content-Type
的值仅限于:text/plain
、multipart/form-data
、application/x-www-form-urlencoded
之一;- 自定义的首部字段不超出CORS安全的首部字段集合范围:
Accept
、Accept-Language
、Content-Type
、Content-Language
- 请求中未使用
Fetch API
的ReadableStream
二进制流对象; -
请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。
- 使用
-
非简单请求方式:
请求头包含
Origin
请求头(包含协议、域名、端口号),用于指示请求来源;服务端设置`Access-Control-Allow-Origin: xxx’,可以设置为*或者指定的域名;
前端不需要特别处理:<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Page Title</title> <meta name="viewport" content="width=device-width, initial-scale=1"> </head> <body> <div class="hook"></div> <script> var target = document.querySelector('.hook'); var xhr = new XMLHttpRequest(); xhr.onload = function(){ if(xhr.readyState == 4) { if(xhr.status >= 200 && xhr.status <300 || xhr.status == 304) { target.innerHTML = xhr.responseText; console.log(xhr.responseText); } else { target.innerHTML = 'Reuqest failed:' + xhr.status; console.log("Request failed:", xhr.status); } } } // xhr.widthCredientials = true; xhr.open('post', 'http://localhost:8080', true); // xhr.setRequestHeader('Access-Control-Allow-Methods', 'OPTIONS, GET, POST') // xhr.setRequestHeader('Content-Type', 'application/x-wwww-urlencoded'); xhr.send('name=fn&id=12377'); </script> </body> </html>
后端需要设定相应的响应头:
var qs = require('querystring'); var http = require('http'); var server = http.createServer(); server.on('request', function (req, res) { var _data = ''; req.on('data', function (chunk){ _data += chunk; }); req.on('end', function () { res.writeHead(200, 'Success', { 'Content-Type': 'application/x-www-urlencoded', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'PUT, GET, POST', 'Access-Control-ALlow-Headers': 'Content-Type', }); res.end("messages from client: \n" + _data); }) }); server.listen(8080); function finishedReq() { console.log('write end...') } console.log('server is running at port 8080...');
以下是一个非简单请求的请求结果:
document.domain + iframe
document.domain跨域的基础是二级域名、协议及端口号要一致,通过document.domain
修改页面的域名,从而达到同一顶级域名下的子域名间的跨域;举个例子:
比如一主页面的域名是sale.game.abc.com
,内嵌一个iframe
,如下:
<iframe id="demo" src="http://gamestatic.abc.com/game/test/a.html">
由于主页面的域名与嵌入的子页面的二级域名是一致的,都是abc.com
,因此可以使用该方法进行跨域;需要在主页面和子页面的脚本中分别使用以下语句将页面的域名置为abc.com
;
document.domain = 'abc.com';
这样,父级页面就可以获取iframe
子页面的document
对象,并且获取和操作子页面的dom元素了;否则,受跨域限制直接在不同子域名下的两个关联页面也无法操作对方的dom;
window.name + iframe跨域
-
window.name特性:
- window.name是一个全局变量;
- 每个窗口有一个独立的window.name与之对应,默认值为空字符串;
- window.name与每个窗口的生命周期相关,同一个窗口载入的多个页面同享同一个
window.name
值,窗口关闭则对应的window.name
也被销毁; - 不同窗口打开的同一域名的两个页面的
window.name
不能共享; window.name
数据格式可以自定义,大小一般不超过2M;
-
跨域原理:利用同一窗口载入的多个页面共享同一个
window.name
,结合iframe
载入不同域的页面,在iframe
子页面内使用window.name=data
赋值,data
是要传递的数据,然后在主页面通过window.name从子页面获取需要的数据;
// 主页面 域名为http://localhost:8001/test/index.html
// iframe内嵌页 域名为http://localhost:8888/test/iframe.html
// proxy.html:代理页是空页面,与主页面同域名
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<p>window.name + iframe</p>
<script>
window.flag = false;
var iframe = document.createElement('iframe');
var loadData = function () {
if (flag) {
var data = iframe.contentWindow.name;
console.log(data);
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
} else {
flag = true;
iframe.contentWindow.location = 'http://localhost:8001/test/proxy.html';
}
}
iframe.src = 'http://localhost:8888/test/iframe.html';
iframe.onload = loadData;
document.body.appendChild(iframe);
</script>
</body>
</html>
// iframe内嵌页 iframe.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script>
window.name = 'message from iframe'
</script>
</body>
</html>
location.hash + iframe
-
跨域原理
location.hash
通过页面链接地址中的hash部分进行数据传递;缺点:数据明文,具有长度限制(收链接长度限制);下边的例子中,index.html
是主页面,域名是http://localhost:8080
;页面内部嵌入了一个src=http://localhost:8888/test/iframe.html#id=123
的iframe
;由于嵌入的iframe
是跨域的,无法直接通信,因此需要一个与index.html
同域名的中转代理页proxy.html
; -
代码实现
// index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <p>window.name + iframe</p> <iframe src="http://localhost:8888/test/iframe.html#id=123" frameborder="0"></iframe> <script> window.onhashchange = function () { console.log(location.hash); } </script> </body> </html>
// iframe.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <script> var proxy = document.createElement('iframe'); proxy.src = 'http://localhost:8001/test/proxy.html#name=fn'; document.body.appendChild(proxy); </script> </body> </html>
// proxy.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <script> console.log('hash: ', location.hash); window.parent.parent.location.hash = self.location.hash; </script> </body> </html>
postMessage
-
postMessage API介绍
语法:发送消息:targetWindow.postMessage(message, targetOrigin, [transfer]);
监听消息: window.onmessage = function (e) { // console.log(e.data); e对象下具有data,source及origin属性}其中,targetWindow是想要通信的其他页面的window对象;targetOrigin是预通信的其他页面的域名,可以设置为
*
与任何页面通信,但出于安全考虑不建议;mesage
是要进行通信的数据,已经可以支持字符串、对象等多种数据类型;transfer
是可选参数,表示一个与message
同时发送到接收方的Transferable对象,控制权由发送方转移到接收方;
onmessage监听消息,回调函数传入对象e,具有属性data
、source
、origin
;data
是传递的数据;source
是对发送方window对象的引用;origin
是发送方的域名; -
代码实现:
// index.html 域名localhost:8001 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <h1>HTML5 postMessage</h1> <p></p> <iframe src="http://localhost:8888/test/sub.html" frameborder="0" onload="load()"></iframe> <script> function load() { var iframe = document.querySelector('iframe'); iframe.contentWindow.postMessage('i need some msg from you.', 'http://localhost:8888'); // window.postMessage({'name': 'post', method: 'cors'}, 'http://localhost:8880/test/sub.html'); window.onmessage = function (e) { console.log(e.data); } } </script> </body> </html>
// sub.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <script> window.onmessage = function (e) { console.log(e.data); e.source.postMessage({'name': 'post', method: 'cors'}, e.origin); } </script> </body> </html>
运行结果:
i need some msg from you. // sub.html输出 {name: "post", method: "cors"} // index.html输出
node中间件
- 原理: 同源策略是浏览器端的安全策略;如果把请求通过node服务器代理请求,那么就没有跨域限制了;
http-proxy-middleware
是node中常用的设置代理的插件库,可以与express、connet等配合使用 - 代码实现:
// server.js const http = require('http'); const server = http.createServer(); const qs = require('querystring'); const url = require('url'); let count= 0; server.on('request', (req, res) => { count++; const query = url.parse(req.url, true).query; res.writeHead(200, { 'Set-Cookie': 'name=fn;Path:/;Domain:localhost:6688;Httponly' }); query.count == 1 ? res.write(`Response from Server -- localhost:6688; visit count: ${count}`) : res.write(`Response from Server -- localhost:6688; no tracking...`); res.end(); }) server.listen(6688); console.log('server is running at port 6688...')
运行结果:// proxy.js const express = require('express'); const proxy = require('http-proxy-middleware'); const app = express(); const options = { target: 'http://localhost:6688', changeOrigin: true, onProxyRes: (proxyRes, req, res) => { res.header('Access-Control-Allow-Origin', 'http://localhost'); res.header('Access-Control-Allow-Credentials', 'true'); proxyRes.headers['x-self-defined'] = 'node middleware'; }, } app.use('/api', proxy(options)); app.use(express.static( './public')); app.listen(8002); console.log('proxy server is listen at port 8002');
nginx反向代理
- 原理:与node中间件的原理一样,都是利用服务端不受同源策略限制的特点,使用同域名的反向代理服务器转发请求,从而进行跨域;
- 实现:
# nginx配置 worker_processes 1; // 工作进程数,与CPU数相同 events { connections 1024; // 每个进程的最大连接数 } http { sendfile on; // 高效文件传输模式 server { listen 80; server_name localhost; # 负载均衡与反向代理 location / { root html; index index.html index.htm; } location /test.html { proxy_pass http://localhost:6688; } } }
此时,直接访问localhost/test.html域名时nginx会将请求代理到localhost:6688域名下,从而实现跨域;// server.js http://localhost:6688 const http = require('http'); const server = http.createServer(); const qs = require('querystring'); const url = require('url'); let count= 0; server.on('request', (req, res) => { // var params = url.parse(req.url, true); var params = qs.parse(req.url.split('?')[1]); res.write(JSON.stringify(params)); res.end(`port: 6688`); }) server.listen(6688); console.log('server is running at port 6688...')
WebSocket
- 介绍:WebSocket是HTML5新增的一种用于客户端和服务端进行全双工通信的协议,可以在客户端和服务端建立一个持久化的连接,并且在一个TCP连接中高效双向数据传递;
- 语法:
var Socket = new WebSocket(url, [protocol] );
- 属性:
readyState
—— 0 (WebSocket.CONNECTING) / 1(WebSocket.OPEN) / 2(WebSocket.CLOSING) / 3(WebSocket.CLOSED);WebSocket.bufferedAmount
—— 已调用send()
方法在缓冲区等待发送的数据字节数; - 事件:
onopen
、onmessage
、onclose
、onerror
; - 方法:
send(data)
、close([code [, reason]])
;可选的code及文本表明连接关闭的原因,默认code=1005;
- 语法:
- 代码实现:
// server.js const http = require('http'); const server = http.createServer(); const url = require('url'); const WebSocket = require('ws'); const socket = new WebSocket.Server({port: 5555}); socket.on('connection', ws => { ws.on('message', (data) => { console.log('data from client: ', data) ws.send('hello websocket...'); console.log('server end send data...'); }) }) console.log('server is running at port 5555...')
// index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Page Title</title> <meta name="viewport" content="width=device-width, initial-scale=1"> </head> <body> <div class="hook"></div> <script> SocketEvent = () => { if (window.WebSocket) { console.log('llll') var socket = new WebSocket('ws://localhost:5555'); socket.onopen = function () { console.log('client socket opening...'); socket.send(`I'm requesting some data from you...`); } socket.onmessage = function (e) { console.log(e); console.log(e.data); } socket.onclose = function (e) { console.log('client socket closing...'); } } } </script> <button onclick="SocketEvent()">socket</button> </body> </html>