什么是跨域
参考 MDN-浏览器的同源策略
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。
同源的定义
如果两个页面的协议、端口(如果有指定)和域名都相同,则这个两个页面具有相同的源。
没有同源策略限制的情况
1、没有同源限制的接口请求
如果没有同源限制的接口请求,将导致CSRF攻击。可以阅读浅谈CSRF攻击方式。简单理解为攻击者盗用了你的身份,以你的名义发送恶意请求。
CSRF攻击攻击原理及过程如下:
- 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;
2.在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A; - 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;
- 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;
- 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。
2、没有同源限制的Dom查询
由于没有同源策略的限制,钓鱼网站可以获取其他网址的Dom嵌在自己写好的iframe里面,这样用户就误以为进入了正确的页面,一旦输入敏感信息就直接被钓鱼网站获取。
跨域的基本方式
我们的目的就是要消除浏览器对我们的误解,从而进行更好的开发。
跨域的几种方式:
- JSONP
- CORS
- webSocket
- document.domain
- Server Proxy
JSONP
JSONP是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。
它的基本思想是,网页通过添加一个<script>元素,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。
本地建一个node服务:
const url = require('url');
require('http').createServer((req, res) => {
const data = {
x: 10
};
const callback = url.parse(req.url, true).query.callback
res.writeHead(200);
//回调原页面上函数处理返回结果
res.end(`${callback}(${JSON.stringify(data)})`);
}).listen(3000, '127.0.0.1');
console.log('服务器已启动...');
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<meta charset="UTF-8">
</head>
<body>
<script>
function jsonpCallback(data){
alert('跨域成功')
}
</script>
<script src="http://127.0.0.1:3000/?callback=jsonpCallback"></script>
</body>
</html>
浏览器打开index.html即可验证跨域成功。
CORS
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。
它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
cors 分为两种请求,简单请求和非简单请求。CORS详解
简单请求
对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。
服务端,Access-Control-Allow-Origin属性,我们需要服务端设置此属性,指定允许的请求源域名,可以通过指定为 *来指定所有域名。
下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。
node服务
var http = require('http');
http.createServer(function (request, response) {
response.writeHead(200, {
'Content-Type': 'text/plain',
'Access-Control-Allow-Origin': '*'
});
response.end('request success!!!');
}).listen(8888);
console.log('Server running at http://127.0.0.1:8888/');
开启node服务
在控制台中发出请求
var url = 'http://127.0.0.1:8888/';
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.send();
非简单请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。即服务器通过返回的头部信息中的Access-Control-Allow-Origin,Access-Control-Allow-Method来告诉浏览器该跨域请求是否被允许。
node代码:
var http = require('http');
http.createServer(function (request, response) {
response.writeHead(200, {
'Content-Type': 'text/plain',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT'
});
response.end('request success!!!');
}).listen(8888);
console.log('Server running at http://127.0.0.1:8888/');
成功跨域。
webSocket实现跨域
WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
WebSocket协议,是建立在TCP协议上的,而非HTTP协议。
ws://127.0.0.1或wss://127.0.0.1就是WebSocket请求。
ws表示WebSocket协议,wss表示加密的WebSocket协议。
WebSocket的好处就是允许服务器和客服端进行实时地互相通信,而不像Ajax那样,只能由客服端发起请求,并且WebSocket不受同源策略限制,这恰恰是Ajax的软肋。
WebSocket采用回调函数监听相应事件,并处理。监听事件一共四个:
1、 onpen :请求成功时,触发
2、 onclose :请求关闭时,触发
3、 onerror :请求或连接期间出错时,触发
4、 onMessage :接收服务器发送来的消息时,触发
当发送请求时,创建的WebSocket 实例,有个状态值readyState:
1、 0 : 代表还没连接或正在连接;
2、 1 : 代表连接成功;
3、 2 : 代表正在关闭连接;
4、 3 : 代表连接关闭。
在该demo中加入这四个监听事件。
node代码:
var WebSocketServer = require('ws').Server;
var wss = new WebSocketServer({port: 8080});
wss.on('connection', function(ws){
ws.on('message', function(message){
var obj = JSON.parse(message);
console.log('received: %s', obj.time);
});
ws.send('hello world');
});
console.log('running!!');
<!DOCTYPE html>
<head>
<title>WebSocket</title>
</head>
<body>
<script>
var onOpen = function(event){
console.log('Socket opened. readyState:'+socket.readyState);
var msg = {
type: "message",
text: "something",
id: "number",
time: Date.now()
};
//send可以向后台发送字符串、Blob或ArrayBuffer,固传入对象时,利用JSON对其序列化
socket.send(JSON.stringify(msg));
};
var onClose = function(event){
console.log('Socket closed.readyState:'+socket.readyState);
console.log('Connected to: ' + event.currentTarget.url);
};
var onMessage = function(data){
console.log("We get signal:");
console.log(data);
console.log('onMessageready: ' + socket.readyState);
};
var onError = function(event){
console.log("We got an error.: " + event.data);
};
var socket = new WebSocket('ws://127.0.0.1:8080/');
socket.onopen = onOpen;
socket.onclose = onClose;
socket.onerror = onError;
socket.onmessage = onMessage;
</script>
</body>
</html>
在RequestHeaders中,”Connection:Upgrade”表示浏览器通知服务器,如果可以就升级为websocket协议。Origin用于验证浏览器域名是否在服务器许可范围内。Sec-WebSocket-Key则用于握手协议密钥,是base64编码的16字节随机字符串。Upgrade头信息表示将通信协议从HTTP/1.1转向该项所指定的协议。
document.domain
同一浏览器的不同页面可以通过传递Cookie,对不同页面进行读写。Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。但是,两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置相同的document.domain就可以共享 Cookie。
Server Proxy
Server Proxy,顾名思义,服务器代理的意思;是由本域服务器请求跨域服务器(服务端不存在同源策略,也就不存在跨不跨域这一说),然后将得到的信息发送给前端,这一过程中页面只是请求了本域服务器,并没有直接和跨域服务器发生直接关系,所以也很好理解这为什么叫服务器代理。
通过例子来理解:
获取 CNode:Node.js专业中文社区 论坛上一些数据,如通过 https://cnodejs.org/api/v1/topics, 但是因为不同域,所以可以将请求后端,让其对该请求代为转发。
const url = require('url');
const http = require('http');
const https = require('https');
const server = http.createServer((req, res) => {
const path = url.parse(req.url).path.slice(1);
if(path === 'topics') {
https.get('https://cnodejs.org/api/v1/topics', (resp) => {
let data = "";
resp.on('data', chunk => {
data += chunk;
});
resp.on('end', () => {
res.writeHead(200, {
'Content-Type': 'application/json; charset=utf-8'
});
res.end(data);
});
})
}
}).listen(3000, '127.0.0.1');
console.log('启动服务,监听 127.0.0.1:3000');
服务器收到请求,会代你发送请求 https://cnodejs.org/api/v1/topics 最后将获取到的数据发送给浏览器。
附:
请求头包含基本内容
- Accept:浏览器可接受的MIME类型(文字、声音、图像等)。
- Accept-Charset:浏览器可接受的字符集(utf-8,ASCII,GB2312等)。
- Accept-Encoding:浏览器能够进行解码的数据编码方式,比如gzip。Servlet+ `能够向支持gzip的浏览器返回经gzip编码的HTML页面。许多情形下这可以减少5到10倍的下载时间。
- Accept-Language:浏览器所希望的语言种类,当服务器能够提供一种以上的语言版本时要用到。
- Authorization:授权信息,通常出现在对服务器发送的WWW-Authenticate头的应答中。
- Connection:表示是否需要持久连接。如果Servlet看到这里的值为“Keep-Alive”,或者看到请求使用的是HTTP 1.1(HTTP 1.1默认进行持久连接),它就可以利用持久连接的优点,当页面包含多个元素时(例如Applet,图片),显著地减少下载所需要的时间。要实现这一点,Servlet需要在应答中发送一个Content-Length头,最简单的实现方法是:先把内容写入ByteArrayOutputStream,然后在正式写出内容之前计算它的大小。
- Content-Length:表示请求消息正文的长度。
- Cookie:这是最重要的请求头信息之一
- From:请求发送者的email地址,由一些特殊的Web客户程序使用,浏览器不会用到它。
- Host:初始URL中的主机和端口。
If-Modified-Since:只有当所请求的内容在指定的日期之后又经过修改才返回它,否则返回304“Not Modified”应答。 - Pragma:指定“no-cache”值表示服务器必须返回一个刷新后的文档,即使它是代理服务器而且已经有了页面的本地拷贝。
- Referer:包含一个URL,用户从该URL代表的页面出发访问当前请求的页面。
- User-Agent:浏览器类型,如果Servlet返回的内容与浏览器类型有关则该值非常有用。
- UA-Pixels,UA-Color,UA-OS,UA-CPU:由某些版本的IE浏览器所发送的非标准的请求头,表示屏幕大小、颜色深度、操作系统和CPU类型。
参考:
超级详细的跨域问题
CSRF
http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html
https://segmentfault.com/a/1190000011162411
https://segmentfault.com/a/1190000015450909
WebSocket
WebSocket教程
https://blog.csdn.net/qq_32842925/article/details/83512309