前言
之所以存在跨域问题,是因为浏览器的同源策略所导致的。
同源策略
同源指的是资源或目标地址的 协议、域名、端口 三者相同,即便是不同的域名指向同一个ip地址,也不同源。
当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域。
为什么要有同源策略
之所以有同源策略是为了保护用户的安全和隐私,不同的网站存储着用户不同的信息,可能存储在本地(cookie、localstorage、indexdb)或者是服务器上面,如果没有同源策略,网站间相互可以获取各自信息,从而导致用户信息泄露造成不可知的损失。
例如:你打开了A网站,并且登录了A网站,A网站也记录了你的cookie信息,然后你打开一个B网站,如果没有同源策略,B网站拿到你的cookie是可以直接请求A网站的接口的,有一些比如个人信息,他就可以通过get等方法,获得到你的信息,甚至可以post等操作去修改你的信息,这样你的账户安全是受到很严重的威胁的。
再例如:如果一个恶意网页嵌套了一个银行的网页的iframe,若果不存在同源策略,那这个恶意网页就能随意获取你在银行网页上的任意用户行为和页面信息,然后他就能获取你输入的账号密码,窃取你的账户信息和钱。
同源策略的限制
不同源,以下行为会收到限制:
Cookie,localStorage,IndexedDB无法读取DOM无法获得Ajax请求发送后被浏览器拦截(并不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了)
注:不管使用哪个协议(HTTP/HTTPS)或端口号,浏览器都允许给定的域以及其任何子域名(sub-domains) 访问 cookie。设置 cookie 时,你可以使用Domain,Path,Secure,和Http-Only标记来限定其访问性。
- 允许跨域资源嵌入(
Cross-origin embedding)
以下是可能嵌入跨源的资源的一些示例:
<script src="..."></script>标签嵌入跨域脚本<link rel="stylesheet" href="...">标签嵌入CSS<img>嵌入图片。支持的图片格式包括PNG,JPEG,GIF,BMP,SVG,...<video>和<audio>嵌入多媒体资源<object>,<embed>和<applet>的插件@font-face引入的字体。一些浏览器允许跨域字体( cross-origin fonts),一些需要同源字体(same-origin fonts)<frame>和<iframe>载入的任何资源。站点可以使用X-Frame-Options消息头来阻止这种形式的跨域交互。
如何跨域
JSONP
因为同源策略允许嵌入跨源的资源,所以可以利用script标签加载脚本的时候在url中传入一个回调函数,在后端响应请求时再返回调用回调函数脚本,浏览器加载完脚本然后执行就能来完成不同域的数据交互。
//index.html
<script type="text/javascript">
function dosomething(data){
//处理获得的数据
}
</script>
<script src="http://example.com:3000/say?callback=dosomething"></script>
//server.js
let express = require('express')
let app = express()
app.get('/say', function(req, res){
let {callback} = req.query
res.end(`${callback}('hello')`)
})
app.listen(3000)
优点:
- 兼容性好
缺点:
- 它支持
GET请求而不支持POST 等其它类行的HTTP请求。 - 它只支持跨域
HTTP请求这种情况,不能解决不同域的两个页面或iframe之间进行数据通信的问题。 JSONP从其他域中加载代码执行,如果该域不安全并且夹带一些恶意代码,会存在安全隐患 要确定JSONP请求是否失败并不容易
CORS
对于跨域请求,请求可能已经发出去了,但是在浏览器收到响应时判断为跨域请求所以将请求的结果给拦截了,而CORS通过后端添加响应允许跨域的一些字段使请求不被拦截实现跨域。
CORS分为简单请求和复杂请求,对于非简单请求会先发一次预检请求:
简单请求
条件:
- 请求方法是
HEAD、GET、POST三种方法之一 - HTTP的头信息不超出以下几种字段
AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type(只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain)
浏览器会在这个请求的头信息中,自动添加一个 Origin 字段来说明本次请求的来源(协议 + 域名 + 端口),而后服务器会根据这个值,决定是否同意这次请求。
如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。 浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。
请求:
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: http://foo.example/examples/access-control/simpleXSInvocation.html
Origin: http://foo.example
响应:
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2.0.61
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
[XML Data]
复杂请求
条件: 不属于简单请求条件的便都是复杂请求
复杂请求如图所示:
预请求
对于非简单请求,浏览器会在正式通信之前,做一次查询请求,叫预请求(preflight),也叫 OPTIONS 请求,因为它使用的请求方式是 OPTIONS。
在OPTIONS请求里,头信息除了有表明来源的 Origin 字段外,还会有一个Access-Control-Request-Method 字段和 Access-Control-Request-Headers 字段,它们分别表明了本次 CORS 请求用到的 HTTP 请求方法和请求会额外发送的头信息字段
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER
如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段:
Access-Control-Allow-Origin: http://example.org
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Access-Control-Max-Age: 1728000
Access-Control-Allow-Origin:该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求Access-Control-Allow-Credentials: 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。Access-Control-Expose-Headers:该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定Access-Control-Max-Age: 该字段可选,用来指定本次预检请求的有效期,单位为秒。在此期间,不用发出另一条预检请求。
withCredentials
CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。另一方面,开发者必须在AJAX请求中打开withCredentials属性。
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
如要发送 Cookie,Access-Control-Allow-Origin 字段就不能设为星号,必须指定明确的、与请求网页一致的域名, 同时,Cookie 依然遵循同源政策,只有用服务器域名设置的 Cookie 才会上传。
postMessage
window.postMessage(message, targetOrigin, [transfer]) 方法是html5新引进的特性,可以使用它来向其它的window对象发送消息,无论这个window对象是属于同源或不同源。利用window.postMessage和window.addEventListener window.onmessage就能实现跨域的数据通信。
window对象可以通过iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames来获得。
它可用于解决以下方面的问题:
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的iframe消息传递
- 上面三个场景的跨域数据传递
// a.html
<iframe src="http://localhost:4000/b.html" id="frame" onload="load()"></iframe>
<script>
function load(params){
let frame = document.getElementById('frame');
//获取iframe中的窗口,给iframe里嵌入的window发消息
frame.contentWindow.postMessage('hello','http://localhost:4000')
// 接收b.html回过来的消息
window.onmessage = function(e){
console.log(e.data)
}
}
</script>
// b.html
<script>
//监听a.html发来的消息
window.onmessage = function(e){
console.log(e.data)
//给发送源回消息
e.source.postMessage('nice to meet you',e.origin)
}
</script>
WebSocket
WebSocket对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。它是基于TCP的全双工通信,即服务端和客户端可以双向进行通讯,并且允许跨域通讯。基本协议有ws://(非加密)和wss://(加密)
//socket.html
let socket = new WebSocket('ws://localhost:3000');
// 给服务器发消息
socket.onopen = function() {
socket.send('hello server')
}
// 接收服务器回复的消息
socket.onmessage = function(e) {
console.log(e.data)
}
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');//npm i ws
// 设置服务器域为3000端口
let wss = new WebSocket.Server({port:3000});
//连接
wss.on('connection', function(ws){
// 接收客户端传来的消息
ws.on('message', function(data){
console.log(data);
// 服务端回复消息
ws.send('hello client')
})
})
nginx
通过nginx配置一个代理服务器(域名与网站域名相同,端口不同)做跳板机,反向代理访问后台接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。
server {
listen 80;
server_name http://www.test.com;
location /api {
# 反向代理地址
proxy_pass http://www.test1.com;
# 修改Cookie中域名
proxy_cookie_domain http://www.test1.com http://www.test.com;
index index.html index.htm;
# 前端跨域携带了Cookie,所以Allow-Origin配置不可为*
add_header Access-Control-Allow-Origin http://www.test.com;
add_header Access-Control-Allow-Credentials true;
}
}
node代理跨域
本质上是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。这里使用 express + http-proxy-middleware 来搭建一个代理服务器, webpack-dev-server 里就是使用的它。
let express = require('express')
let proxy = require('http-proxy-middleware')
let app = express()
app.use('/api', proxy({
// 代理跨域目标接口
target: 'http://www.test1.com',
changeOrigin: true,
// 修改响应头信息,实现跨域并允许带cookie
onProxyRes: function(proxyRes, req, res) {
res.header('Access-Control-Allow-Origin', 'http://www.test.com')
res.header('Access-Control-Allow-Credentials', 'true')
},
// 修改响应信息中的cookie域名,为false时,表示不修改
cookieDomainRewrite: 'http://www.test.com'
}))
app.listen(3000)