1、什么是跨域?
跨域(cross-origin)其实就是跨源,是指的浏览器不能执行其它网站的脚本,是浏览器对JavaScript实施的安全限制。跨域由于浏览器的同源策略所导致的。同源策略要求源相同才能正常进行通信。
跨域只存在于浏览器端,不存在于安卓/ios/Node.js/python/java等其它环境
同源策略
同源策略是一个重要的安全策略,它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。
源(origin)是什么?什么是同源?
URL | 说明 | 是否跨域,能否通信 | |
---|---|---|---|
http://www.doman.com/a.js http://www.doman.com/b.js |
协议、域名、端口相同 | 否,可通信 | |
http://www.doman.com:8000/a.js http://www.doman.com/b.js |
协议、域名相同 端口不同 |
跨域,不可通信 | |
http://www.domain.com http://x.doman.com http://doman.com |
协议与端口相同 域名不同 |
跨域,不可通信 | |
http://www.domain.com https://www.domain.com |
协议与端口不同,域名相同 (http 默认为 80 端口,https 默认为 443 端口) |
跨域,不可通信 | |
http://www.domain.com http://192.168.3.1:80 |
协议与端口相同 域名不同 |
跨域,不可通信 |
2、跨域限制了什么?
同源策略限制了不同源之间的交互,交互分为三种情况:
跨域写操作:一般被允许,例如重定向和表单提交。例如:在http://www.google.com 中打开控制台输入 window.location.href = ‘http://www.baidu.com',此时不受限制。
跨域资源嵌入:一般被允许。主要指的是 script、img、link 等带有 src 属性的标签不受同源策略限制。
跨域读操作:一般不被允许。指的就是执行其他网站的 JS 脚本。例如:海鸥系统中嵌入了合同系统,在合同源下输入 window.parent.localStorage.getItem(‘com.sankuai.it.jwl.app_ssoid’) ,此时 window.parent 与 window.self 不同源,则这句脚本中 window.parent.localStorage.getItem(‘com.sankuai.it.jwl.app_ssoid’) 实际上是执行了另一个源的脚本。
同源策略主要限制了两种场景:
- DOM 读取与属性设置
- BOM 读取(一般主要关注:localStorage、IndexedDB 的读取)
- 服务器资源的读取(client to server):不能向不同源的服务器发送 XMLHttpRequest(一般被封装为 Ajax)请求(实际上可以发送请求,服务器也可正常返回,但是浏览器会拦截)
DOM:
BOM
3、为什么需要跨域限制?
为什么需要限制本地数据(DOM和BOM)的读取?
假设没有本地数据的限制,那么黑客可以通过嵌套 iframe 或者 window.open 的方式,取到其他域下的信息(例如:账号、密码等)。
为什么需要限制服务器资源(Ajax请求)的读取?
假如不限制服务器资源的读取,那么黑客可以进行 CSRF(跨站请求伪造)攻击。(TODO)
4、如何解决 Ajax 请求跨域问题(client to server)
4.1 CORS(通过 HTTP 头实现)
定义:跨源资源共享 (CORS) (或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其它 origin(域,协议和端口),这样浏览器可以访问加载这些资源。
特点:CORS 更依赖于后端,针对 cookie 设置需要浏览器和后端同时支持。
简单请求和复杂请求:CORS 中根据请求方法分为简单请求和复杂请求两种情况。
简单请求
同时满足以下场景的请求均为简单请求。参考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS
- 简单请求的请求方法只能为 GET、HEAD 或 POST;
- 简单请求的 HTTP 头部仅允许人为设置 Accept/Accept-Language/Conent-Language/Content-Type,不可再设置其他字段;人为设置表示的是除了用户代理中自动设置的(eg. Connection、User-Agent 等)以及 Fetch 规范中定义为禁用的字段
- 简单请求的 Content-Type 的值仅限于下列三者之一:text/plain、multipart/form-data、application/x-www-form-urlencoded;
- 请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器
- XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问
- 请求中没有使用 ReadableStream 对象
简单请求实现跨域,只需要设置 Access-Control-Allow-Origin 即可:
// 来自以下 Access-Control-Allow-Origin 源的请求不受跨域限制
Access-Control-Allow-Origin: <origin> | *
复杂请求
除了简单请求的就是复杂请求。
复杂请求相对于简单请求而言,需要先发送一次预检请求(preflight),该请求使用 OPTIONS 方法,返回 204,预检请求的作用是为了询问服务器是否允许本次跨域请求。如果返回 204 则表示后续的请求都禁止了跨域的限制(有时间期限),否则则表示不允许后续的请求。
复杂请求实现跨域需要服务器设置下述几个属性:
// 来自以下 Access-Control-Allow-Origin 源的请求不受跨域限制
Access-Control-Allow-Origin: <origin> | '*'
// 在预检请求的响应头中出现,告知服务器,实际的请求中会使用的请求方法,在使用了非 GET、HEAD 和 POST 方法时需要设置
Access-Control-Allow-Methods: <method>
// 在预检请求的响应头中出现,列出了将会在正式请求的 Access-Control-Request-Headers 字段中出现的首部信息,在设置了额外的头部信息时需要设置
Access-Control-Allow-Headers: <header-name>[, <header-name>]*
demo
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>cors demo</title>
</head>
<body>
<button id="getlist1">获取列表1-简单请求</button>
<button id="getlist2">获取列表2-复杂请求</button>
<script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
<script>
// 简单请求
getlist1.onclick = () => {
axios.get('http://127.0.0.1:9090/api/corslist').then(res => {console.log('res', res.data)});
}
// 非简单请求
getlist2.onclick = () => {
axios.get('http://127.0.0.1:9090/api/corslist', {
headers: {
cc: 'text/html' // 自定义了一个头部信息,故为非简单请求
}
})
}
</script>
</body>
</html>
const Koa = require('koa');
const router = require('koa-router')();
const app = new Koa();
app.use(async (ctx, next) => {
ctx.set('Access-Control-Allow-Origin', ctx.headers.origin);
ctx.set('Access-Control-Request-Method', 'PUT,POST,GET,DELETE,OPTIONS')
ctx.set('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, cc')
// 如果是预检请求
if (ctx.method === 'OPTIONS') {
ctx.status = 204;
return;
}
await next();
});
router.get('/api/corslist', async (ctx) => {
ctx.body = {
data: [{name: '第一项'}, {name: '第二项'}, {name: '第三项'}]
}
})
app.listen(9090);
优缺点
优点
- CORS 支持所有类型的 HTTP 请求。
- CORS 与 XMLHttpRequest 的请求代码一致,容易维护。
缺点
- 第一次发送复杂时会多一次预检请求。
- 兼容性问题,IE<=9, Opera<12, or Firefox<3.5 此时无法使用该方法,可考虑使用 JSONP 的方案。
4.2 JSOP(script 无跨域限制)
1)实现原理:利用了 script 标签中 src 属性无跨域限制的特性。
具体步骤:
- 前端定义解析后端返回数据的函数,例如:jsonpCallback;
- 将该解析函数通过参数的形式拼接在请求的接口中,并且声明该函数,例如:http://localhost:9090/api/jsonp?cb=jsonpCallback
- 后端通过接口 url 获取前端声明的解析函数,带上相应的后端数据,并且以立即执行函数的形式作为返回体返回给前端,例如:返回体为
jsonpCallback(msg)
- 前端接收到后端的内容后,会立即执行该解析函数,取得相应的数据
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>cors demo</title>
</head>
<body>
<script>
window.jsonpCallback = (res) => {
console.log('res', res)
}
</script>
<script src="http://localhost:9090/api/jsonp?msg=hello&cb=jsonpCallback" type="text/javascript"></script>
</body>
</html>
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
if (ctx.path === '/api/jsonp') {
const { cb, msg } = ctx.query;
console.log('jsonp server: cb ', cb, ', msg ', msg);
// 返回给前端一个立即执行的函数,即 jsonpCallback(msg)
ctx.body = `${cb}(${JSON.stringify({ msg })})`;
return;
}
});
app.listen(9090);
优点和缺点:
优点:无兼容问题
缺点:仅支持 get 方法
4.3 Nginx 反向代理(服务器向服务器发起请求)
1)实现思路:跨域是由于浏览器的同源策略引起的,而同源策略只存在于浏览器端,因此服务器请求不同源的服务器是不会有跨域问题的,因此可以通过 Nginx 作为代理服务器,代替浏览器发送请求到相应的服务器。
2)Demo
- Nginx 服务的源为 http://localhost:9080
- 网站的源为 http://localhost:9000
- 后端服务源为 http://localhost:9090
Nginx
server {
listen 9080;
server_name localhost;
// 当访问 http://localhost:9080/nginx 时会转发到 http://localhost:9000/nginx
location / {
proxy_pass http://localhost:9000;
}
// 当访问的 path 中是以 /api 开头的,都会转发到 http://localhost:9090/api 后端服务上
location /api {
proxy_pass http://localhost:9090/api;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>nginx demo</title>
</head>
<body>
<button id="getlist">获取列表</button>
<script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
<script>
getlist1.onclick = () => {
axios.get('http://localhost:9080/api/corslist').then(res => {console.log('res', res.data)});
}
</script>
</body>
</html>
const Koa = require('koa');
const router = require('koa-router')();
const koaBody = require('koa-body');
const app = new Koa();
app.use(async (ctx, next) => {
await next();
});
app.use(koaBody({ multipart: true }));
router.get('/api/corslist', async (ctx) => {
console.log('ctx', ctx)
ctx.body = {
data: [{name: '第一项'}, {name: '第二项'}, {name: '第三项'}]
}
})
app.use(router.routes());
app.listen(9090);
优点与缺点:
优点:配置简单,只需要设置 nginx 配置,无需在前端和后端做特殊处理。
缺点:
- 后期维护成本高。
- 不同环境服务域名可能不一致,因此nginx配置也各不相同,不便于移植。
4.4 WebSocket 协议(换协议)
1)定义:Websocket 是 HTML5 标准中的一个通信协议协议,以ws://(非加密)和wss://(加密)作为协议前缀,该协议不实行同源政策,只要服务器支持就行。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>websocket</title>
</head>
<body>
<script>
// 创建 websocket 链接
let socket = new WebSocket('ws://localhost:9090');
// 当连接成功时触发
socket.onopen = function () {
console.log('Connectiong open...')
socket.send('Hello server!');
}
// 监听服务器返回的消息
socket.onmessage = function (e) {
console.log('message', e.data);
}
</script>
</body>
</html>
const WebSocket = require("ws");
// 启动一个 websocket 服务
const server = new WebSocket.Server({ port: 9090 });
// 服务器连接成功
server.on("connection", function (socket) {
// 通过监听 message,返回数据
socket.on("message", function (data) {
socket.send(data);
});
});
缺点:
- 兼容问题,可引入 socket.io 进行兼容处理
- 需要服务器支持
6、如何解决完全不同源的跨域(client to client)
6.1 postMessage
1)定义:postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多可以跨域操作的window属性之一。
2)语法:otherWindow.postMessage(message, targetOrigin, [transfer]);
- otherWindow:是其他窗口的引用,比如 iframe 的 contentWindow 属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames;
- message:将要发送给其他 window 的数据;
- targetOrigin:指定哪些 origin 的窗口能接收到消息事件,设置为 “*” 时表示无限制;
- transfer:是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
References
https://mp.weixin.qq.com/s/4XK45ta76HvY1LQ2GDJQzQ
https://segmentfault.com/a/1190000015597029
https://juejin.cn/post/6844903553069219853#comment
https://juejin.cn/post/6850037265595858952