跨域,是指浏览器不能执行其它网站的脚本。它是由浏览器的同源(所谓同源,就是协议、域名、端口均相同)策略造成的,是浏览器对Javascript
实施的安全限制。
比如,站点
http://domain-a.com
的某 HTML 页面通过<img>
的 src 请求http://domain-b.com/image.jpg
,这就发起了一个跨域HTTP请求。
随着互联网的发展,同源策略严重影响了项目之间的连接,尤其是大项目,需要多个域名配合完成,因此W3C推出了CORS
(Cross-origin resource sharing,跨域资源共享)。
CORS的基本思想就是使用额外的HTTP头部让浏览器与服务器进行沟通,从而决定是否接受跨域请求。跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器,让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。
网络上的许多页面都会加载来自不同域的CSS样式表,图像和脚本等资源。出于安全原因:
例如,
XMLHttpRequest
和Fetch API
遵循同源策略。这意味着使用这些API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源,除非响应报文包含了正确CORS响应头。
跨域资源共享( CORS )机制允许 Web 应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行。现代浏览器支持在 API 容器中(例如 XMLHttpRequest
或 Fetch
)使用 CORS,以降低跨域 HTTP 请求所带来的风险。
CORS请求失败会产生错误,但是为了安全,在
JavaScript
代码层面是无法获知到底具体是哪里出了问题,只能查看浏览器的控制台以得知具体是哪里出现了错误。
浏览器在跨域访问时,会自动添加HTTP头信息,或者发起预检请求,用户对此毫无感知。因此是否支持跨域请求,关键在于服务器是否做了CORS配置,允许跨域访问。
浏览器将跨域请求分为两类:简单请求和非简单请求(预检请求)。对于这两种请求,浏览器的处理方式是不一样的。
简单请求的请求方法只能是:
GET
POST
HEAD
简单请求的请求头只能是:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type
的值只能是:application/x-www-form-urlencoded
multipart/form-data
text/plain
DPR
Downlink
Save-Data
Viewport-Width
Width
简单请求中的任意XMLHttpRequestUpload
对象均没有注册任何事件监听器;XMLHttpRequestUpload
对象可以使用 XMLHttpRequest.upload
属性访问。
简单请求中没有使用 ReadableStream
对象。
对于简单请求,浏览器采用先请求后判断的方式,即浏览器直接发出CORS请求,在请求头中增加Origin
字段来向服务器说明,本次请求来自于哪个源(协议+域名+端口),服务器决定是否允许这个源的访问。
Access-Control-Allow-Origin
字段,如果没有,浏览器就知道服务器是不允许跨域访问的,就会抛出错误字段名 | 字段值 | 描述 |
---|---|---|
Access-Control-Allow-Credentials | true | 布尔值,表示是否允许发送Cookie,默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器 |
Access-Control-Allow-Headers | Content-Type、Content-Length、Authorization、Accept、X-Request-With、Cookie | 允许浏览器在CORS中发送的头信息 |
Access-Control-Allow-Methods | GET、PUT、POST、DELETE、OPTIONS | 允许浏览器在CORS中使用的方法 |
Access-Control-Allow-Origin | 请求头中Origin字段的值或者* | * 表示接受任意域名的请求,如果服务端指定了具体的域名而非* ,那么响应头中的Vary 字段的值必须包含Origin 。这将告诉客户端:服务器对不同的源站返回不同的内容 |
对于非简单请求,浏览器采用预检请求,询问服务器是否支持跨域请求。
OPTIONS
请求,询问服务器当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP方法和头字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest
请求,否则就报错。在这个OPTIONS请求中,包含:
用于询问服务器支持跨域访问的请求头和请求方法。
字段名 | 字段值 | 描述 |
---|---|---|
Access-Control-Allow-Credentials | true | 布尔值,表示是否允许发送Cookie,默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器 |
Access-Control-Allow-Headers | Content-Type、Content-Length、Authorization、Accept、X-Request-With、Cookie | 允许浏览器在CORS中发送的头信息 |
Access-Control-Allow-Methods | GET、PUT、POST、DELETE、OPTIONS | 允许浏览器在CORS中使用的方法 |
Access-Control-Allow-Origin | 请求头中Origin字段的值或者* | * 表示接受任意域名的请求,如果服务端指定了具体的域名而非* ,那么响应头中的Vary 字段的值必须包含Origin 。这将告诉客户端:服务器对不同的源站返回不同的内容 |
Access-Control-Max-Age | 3600 | 用来指定本次预检请求的有效期,单位为秒。如有效期是3600秒,即允许缓存该条回应3600秒,在此期间,可直接发送正式请求,不用再发预检请求 |
cookie 属性:
path
domain
expire
HttpOnly
Secure
SameSite
,一种新的防止跨站点请求伪造(CSRF)的HTTP安全特性Chrome 80 默认将没有设置SameSite
设置为SameSite=Lax
。
SameSite的值 | 对应的描述 |
---|---|
Strict | 最为严格,完全禁止第三方Cookie,跨站点时,任何情况下都不会发送Cookie |
Lax | 稍稍放宽,大多数情况也是不发送第三方Cookie,但是导航到目标网址的 Get 请求除外,见下面表格 |
None | 网站可以选择显式关闭SameSite属性,将其设为None。前提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效 |
导航到目标网址的 GET 请求,只包括三种情况:链接,预加载请求,GET 表单。详见下表。
请求类型 | 示例 | 正常情况 | Lax |
---|---|---|---|
链接 | <a href="..."></a> | 发送 Cookie | 发送 Cookie |
预加载 | <link rel="prerender" href="..."/> | 发送 Cookie | 发送 Cookie |
GET 表单 | <form method="GET" action="..."> | 发送 Cookie | 发送 Cookie |
POST 表单 | <form method="POST" action="..."> | 发送 Cookie | 不发送 |
iframe | <iframe src="..."></iframe> | 发送 Cookie | 不发送 |
AJAX | $.get("...") | 发送 Cookie | 不发送 |
Image | <img src="..."> | 发送 Cookie | 不发送 |
在用户浏览器支持 SameSite
属性的情况下,设置了Strict
或Lax
以后,基本就杜绝了CSRF 攻击。
遇到跨域的报错,可以分别从客户端和服务端去解决。
跨域的判断是在浏览器进行的,服务器只是根据客户端的请求做出正常的响应,服务端不对跨域做任何判断。因此如果禁用了浏览器的跨域检查,使浏览器不再对比Origin
是否被服务器允许,即可发出正常的请求。
Chrome 80 版本开始,跨站访问时直接不携带cookie
进行请求的发送,其他的浏览器可以正常访问。因此,定位到和浏览器版本有关,新增了一个cookie
的属性,samesite
。
需要所有客户都修改浏览器的设置,因此只在开发调试的过程中使用,如:给Chrome
浏览器设置--disable-web-security
启动参数。
chrome://flags/
SameSite by default cookies
、Cookies without SameSite must be secure
Disable
127.0.0.1.1:8888 api.test.com // 把本地请求转向到api接口
增加代理服务器,和发起跨域请求的服务器放在同一个域名下,接口请求全走代理服务器,这样就变成了同源访问,不存在跨域访问,因此就不会存在跨域的问题。该方式中,所有发往目标服务器的数据,都会经过代理服务器,适用于同一个公司内部不同域名之间相互访问的情况。
使用此方式需注意代理服务器的性能,应与后端的目标服务器的性能相匹配,否则代理服务器会成为整个系统的性能瓶颈。
// webpack
devServer: {
port: 8000,
proxy: {
"/api": {
target: "http://localhost:8080"
}
}
}
// Vue-cli 2.x
proxyTable: {
'/api': {
target: 'http://localhost:8080',
}
}
// Vue-cli 3.x vue.config.js
devServer: {
port: 8000,
proxy: {
"/api": {
target: "http://localhost:8080"
}
}
}
// Parcel 2.x .proxyrc
{
"/api": {
"target": "http://localhost:8080"
}
}
# Nginx
server {
listen 80;
server_name local.test;
location /api {
proxy_pass http://localhost:8080;
proxy_cookie_domain : localhost:8080 local.test; //当reseponse的set-cookie中设置domain时才需要配置此项, 用于修改set-cookie的domain指向
}
location / {
proxy_pass http://localhost:8000;
}
}
在目标服务器上配置CORS响应头,这样浏览器经过对比判断之后,就可以发起正常的访问。
add_header Access-Control-Allow-Origin *
add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS
add_header Access-Control-Allow-Headers *
此方式的优点是不用修改应用代码,缺点是不能做细粒度的编程,从而做到细粒度的控制,如根据请求参数的不同而返回不同的结果。
由于是通过代码控制,因此可以实现细粒度的控制,在解决跨域问题的同时,可以满足复杂的业务需求。
app.use(async (ctx, next) => {
ctx.set("Access-Control-Allow-Origin", ctx.headers.origin);
ctx.set("Access-Control-Allow-Credentials", true);
ctx.set("Access-Control-Request-Method", "PUT,POST,GET,DELETE,OPTIONS");
ctx.set(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept"
);
if (ctx.method === "OPTIONS") {
ctx.status = 204;
return;
}
await next();
});
// 或者使用现成的库
const cors = require("koa-cors");
app.use(cors());
JSONP(JSON with Padding)是JSON的一种“使用模式”, 利用 script
标签没有跨域限制的特性, 解决跨域问题。
<!-- HTML-->
<script src="http://127.0.0.1:3000/list?callback=func"></script>
<script>
function func(res){
//处理res
}
</script>
// javascript
let express = require('express'),
app = express()
app.listen(8888, _=>{
console.log('ok')
})
app.get('/list', (req, res)=>{
let { callback = function(){} } = req.query;
let data = {
code: 0,
message: '返回数据'
}
res.send(`${callback}(${JSON.stringify(data)})`)
})
window.postMessage()
方法可以安全地实现跨源通信。
应用场景:
otherWindow.postMessage(message, targetOrigin, [transfer]);
Transferable
对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权<!-- a页面 -->
<iframe01-CORS和Cookie
src="http://localhost:8080"
frameborder="0"
id="iframe"
onload="load()"
></iframe>
<script>
function load() { //消息传递需要写在onload事件中
iframe.contentWindow.postMessage("消息", "http://localhost:8080"); //往b发送消息
window.onmessage = e => {
console.log(e.data); //接收b返回的消息
};
}
</script>
<!-- b页面 -->
<div>hello</div>
<script>
window.onmessage = e => { //监听来自a的消息
console.log(e.data);
e.source.postMessage('返回消息:' + e.data, e.origin); //往a返回消息
};
</script>
不同源的网站依旧可以通过iframe方式引入到html, 但需要一些额外处理才能获取想要传递的信息, 这就出现了下面三种基于iframe的跨域方式。
只能适用于二级域名相同的情况下,比如 a.test.com
和 b.test.com
, 只需要给两个页面同时添加 document.domain ='test.com'
表示二级域名都相同就可以实现跨域。
<!-- a.test.com -->
<body>
<iframe
src="http://b.test.com/b.html"
frameborder="0"
onload="load()"
id="frame"
></iframe>
<script>
document.domain = "test.com"; //设置主域
function load() {
console.log(frame.contentWindow.qqq);
}
var aaa = 999;
</script>
</body>
<!-- b.test.com -->
<body>
hellob
<script>
document.domain = "test.com";//设置主域
var qqq = 100;
console.log(window.parent.aaa)
</script>
</body>
原理就是通过 url 带 hash ,通过一个非跨域的中间页面来传递数据,但是hash
是在地址栏中的, 能传递的数据有限, 例如chrome < 8k, ie < 2k。
<!-- a.html 和 b.html 是同源的,都是http://localhost:8000,而 c.html 是http://localhost:8080 -->
<!-- a.html -->
<iframe src="http://localhost:8080/hash/c.html#name1"></iframe>
<script>
console.log(location.hash);
window.onhashchange = function() {
console.log(location.hash);
};
</script>
<!-- b.html -->
<script>
window.parent.parent.location.hash = location.hash;
</script>
<!-- c.html -->
<body></body>
<script>
console.log(location.hash);
const iframe = document.createElement("iframe");
iframe.src = "http://localhost:8000/hash/b.html#name2";
document.body.appendChild(iframe);
</script>
window 对象的 name
属性是一个很特别的属性,当该 window 的 location
变化,然后重新加载,它的 name
属性可以依然保持不变。
a跨域访问c, 之后再指向同源的b (注意,这个重新指向在onload事件中只要执行一次, 否则会造成无限循环), 只要b中没有修改window.name
, window就会保持 c 页面中的值。
<!-- a.html 和 b.html 是同源的,都是http://localhost:8000,而 c.html 是http://localhost:8080 -->
<!-- a.html -->
<iframe
src="http://localhost:8080/name/c.html"
frameborder="0"
onload="load()"
id="iframe"
></iframe>
<script>
let first = true;
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
function load() {
if (first) {
// 第1次onload(跨域页)成功后,切换到同域代理页面
//此处转向只要执行一次,否则相当于一直加载iframe,一直调用onload事件, 这样就是死循环
iframe.src = "http://localhost:8000/name/b.html";
first = false;
} else {
// 第2次onload(同域b.html页)成功后,读取同域window.name中数据
console.log(iframe.contentWindow.name);
}
}
</script>
<!-- b.html -->
<div></div>
<!-- c.html -->
<script>
window.name = "ccc";
</script>
可以在 http 返回头 添加
X-Frame-Options: SAMEORIGIN
防止被别人添加至 iframe。
使用Websocket而不使用HTTP, 就不会有跨域的限制。
<script>
let socket = new WebSocket("ws://localhost:8080");
socket.onopen = function() {
socket.send("消息");
};
socket.onmessage = function(e) {
console.log(e.data);
};
</script>
const WebSocket = require("ws");
const server = new WebSocket.Server({ port: 8080 });
server.on("connection", function(socket) {
socket.on("message", function(data) {
socket.send(data);
});
});