一次前后端联调的过程中,后端发现无论它代码怎么写,都没有成功把cookie写进页面。事后发现是前端的锅,前端没有进行跨域处理。
于是这位前端同学,打算好好地学一下跨域。然后发现跨域的图谱由点到面,竟不只有一个知识点要学。
一、什么叫跨域?跨域问题又从何说起?
当我们要跨过某一样的东西的时候,我们可以确认的是——肯定有东西在阻挡。
这里发起阻挡的对象是——浏览器。
浏览器出于安全的目的,它不允许不满足“同源策略”的站点A访问站点B的资源。
“跨域问题”其实更应该叫做“跨源问题”。
1.1“同源策略”—何谓同源,同源策略限制了什么
(他就是一个类似资源访问“白名单”的东西)
- 协议一样:例如都是http
- 域名一样:例如都是 www.baidu.com
- 端口一样:例如都是8080端口,或者都是80默认端口
以上三者也是们的URL的组成部分之一(协议+域名+端口+资源文件名字)
浏览器的“同源策略”限制了不同源之间的交互(例如发请求XMLHttpRequest)。
但这里必须注意一点:例如在JS脚本内发请求,“同源策略”对请求**限制的是,不让发起方获得请求结果,而不是直接阻断发起方——发起请求——这意味着实际上不同源的服务器,收到了请求,只是返回的结果,被浏览器阻断了。
浏览器的策略本质是:一个域名下面的JS,没有经过允许是不能读取另外一个域名的内容,但是浏览器不阻止你向另外一个域名发送请求。
MDN完整同源策略定义:https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy
里面有更细的内容,关于源的继承、源的更改。 迫于其中细节不是特别重点,这里不阐述。
1.2 谁受“同源策略”的影响
- 【DOM操作】(例如嵌入页面的iframe页面,默认不允许操作原页面的dom结构)
【网络请求】XMLHttpRequset/Fetch
可以正常发送请求,但是浏览器会阻断服务器的返回信息
【数据——LocalStorage、SessionStorage、indexedDB、Cookie】不能跨域读取
1.3 谁不受“同源策略”的影响
以下的不受主要是,一开始指定策略的时候 Chrome 可能考虑了灵活性与安全性的权衡。
【一些跨域的写操作】例如 location 的重定向、a标签资源跳转
【一些跨域资源嵌入标签】script、img、video对资源的引用
例如
【一些未被考虑进去的协议】websocket
二、为什么我们常常会遇见跨域的场景
作为前端,很多时候,我们与后端联调的时候,是需要跨域的。
因为通常,我们前端的项目,跑在我们自己本机的IP上,比如
172.17.191.1:8080
而后端同学他们的后端代码,通常也是跑在本机
172.17.191.33:8080
哪怕是跑在测试环境(测试服务器),那IP肯定也和我们前端同学不一样
test.hh.com
两个人的电脑就是两个IP地址,所以,如果你作为前端,用**你本机的项目**,与后端同学的项目(或者与服务器跑起来的项目),必然会遭遇跨域问题
(如果你没有遭遇过,说明该问题被下面的解决方案解决了)
**
当你发现有以下情况,可能就是你(或者后端)的“跨域工作”没有做好
- 前端访问不了后端的接口
- 后端发现设置不了cookie
三、常用的跨域的解决方案(部分有具体代码)
其实这些解决方案可以非常好的分类
其实当理解了浏览器的同源策略后,我们就能发现、或者直观地得出几种解决方案了。
- 针对网络请求
- 既然浏览器的“同源策略”限制了,我们就想办法绕开浏览器的同源策略。(服务器代理、直接关闭Chrome的安全检查)
- 既然存在不受“同源策略”的标签,我们利用这些标签不就行了吗!机智如我们!!(script-JSONP、img-图片探测)
- 使用官方开的后门——“CORS”,为跨域而生,不说了吧,官方的。
- 特殊:Websocket(设计之初,他就不受“同源策略”限制——游离三界外,不在五行中)
- 针对DOM操作
我们先用 koa 开一个文件服务器
const koa = require('koa')
const koaStatic = require('koa-static')
const app = new koa()
app.use(koaStatic(__dirname))
app.listen(3030)
然后在 http://127.0.0.1:3030/otherIndex.html 中写入下面的 js 代码
(注意,该otherIndex.html也要放在和上面的服务器文件在一起,因为我上面用的 __dirname)
fetch('http://127.0.0.1:8002/test')
.then(res => res.json())
.then(data => console.log('data', data))
可以看到,我们用
源:http://127.0.0.1:3030 访问了 源:http://127.0.0.1:8002
源8002的服务器代码
const koa = require('koa')
const koaStatic = require('koa-static')
const app = new koa()
app.use(koaStatic(__dirname))
app.use(async ctx => {
console.log('收到了请求')
ctx.body = 'test'
})
app.listen(8002)
这样的结果是什么呢?
如果我们没有关闭chrome的默认安全策略:
接下里,让我们对 chrome 的安全策略进行关闭
- c盘(或者其他地方创建个用户数据文件夹)MyChromeDevUserData
修改Chrome启动图标中的目标
C:\Users\Administrator\AppData\Local\Google\Chrome\Application\chrome.exe —disable-web-security —user-data-dir=C:\MyChromeDevUserData用这个修改版的Chrome快捷方式打开目标页面,看见这个提示就说明你chrome修改对了
- 再次打开页面,我们先原来的跨域报错没有了,可以正常访问
3.2 使用服务器代理(真·绕过浏览器)
这里其实利用了服务器不受跨域限制的原理。 既然浏览器不让,我们就: 浏览器 -> 同域代理服务器 -> 目标服务器
我们启动一个Nginx服务器,配置下代理
nginx.conf
server {
listen 3030;
server_name localhost;
location ^~/my/{
proxy_pass http://127.0.0.1:8002;
}
location / {
root html;
index otherIndex.html;
}
}
otherIndex.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
fetch('/my/test')
.then(res => res.json())
.then(data => console.log('data', data))
</script>
</html>
3.3 JSONP(利用漏洞——不受“同源策略”影响的标签)
主要思路:
- 创建一个script标签,通过修改 src 为我们get请求的路径,参数也传在 src 中,
- 服务器对指定路由做处理(对接前端的src)
- 拿到数据后删掉 script,收尾
前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
function jsonp({ url, params, callback }) {
return new Promise((resolve, reject) => {
let script = document.createElement('script')
// 定义一个全局方法来接收,等后端返回-> 则执行,同步执行后,删除多创建的 script
window[callback] = function (data) {
console.log('看看后端传回的数据:', data)
resolve(data)
document.body.removeChild(script)
}
params = { ...params, callback }
let arrs = []
for (let key in params) {
arrs.push(`${key}=${params[key]}`)
}
script.src = `${url}?${arrs.join('&')}`
document.body.appendChild(script)
})
}
jsonp({
url: 'http://localhost:8002/say',
params: { data: '前端传点什么呢' },
callback: 'show'
}).then(data => {
console.log(data)
})
</script>
</html>
koa代码
let koa = require('koa')
let app = new koa()
app.use(async ctx => {
if (ctx.path === '/say') {
// 接收前端传回的参数
const {callback} = ctx.query
let params = {
data: '后端想传过去的数据'
}
ctx.body = `${callback}(${JSON.stringify(params)})`
}
})
app.listen(8002)
最终效果: http://127.0.0.1:3030/jsonpClient.html -> 跨域 -> http://localhost:8002/say
成功!!!
3.3.other 弱化版,利用 img 标签
这里提一嘴,这里也有弱化版,利用图片,但是只能发送请求,拿不到返回结果(无法像script一样,拿到结果,并利用结果来“执行函数”)
用来统计一下网站的访问次数(偷看用户当前浏览器的Cookie还是可以的)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
function jsonp({ url, params, callback }) {
return new Promise((resolve, reject) => {
let img = new Image()
window[callback] = function (data) {
document.body.removeChild(img)
}
img.onload = img.onerror = function(event) {
console.log('请求返回了')
window
}
params = { ...params, callback }
let arrs = []
for (let key in params) {
arrs.push(`${key}=${params[key]}`)
}
img.src = `${url}?${arrs.join('&')}`
document.body.appendChild(img)
})
}
jsonp({
url: 'http://localhost:8002/say',
params: { data: '前端传点什么呢' },
callback: 'show'
}).then(data => {
console.log(data)
})
</script>
</html>
3.4 Websocket(利用漏洞——不受“同源策略”影响的协议)
启动一个 WebSocket 服务器
const Koa = require('koa');
const WebSocket = require('ws');
const app = new Koa();
const ws = new WebSocket.Server({ port: 8888 });
ws.on('connection', ws => {
console.log('server connection');
ws.on('message', msg => {
console.log('server receive msg:', msg);
});
ws.send('Information from the server');
});
app.listen(8002)
启动一个文件静态服务器,打开在 http://127.0.0.1:3030/otherIndex.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
let socket = new WebSocket('ws://127.0.0.1:8888')
socket.onopen = function () {
console.log('socket open')
}
socket.onmessage = function(event) {
console.log('data', event.data)
}
socket.onerror = function (error) {
console.log('error:', error)
}
</script>
</html>
我们发现,
http://127.0.0.1:3030 -> WebSocket -> ws://127.0.0.1:8888
成功了!
说明跨域成功。
3.5 CORS(官方推出的跨域方案)
3.6 【针对DOM】的跨域,postMessage
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'aym',
type:'wuhan'
};
// 向domain2传送跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://10.73.154.73:8088');
};
// 接受domain2返回数据,这边给延迟的原因,因为同步传输时,页面不一定立马拿到数据,所以给延迟
setTimeout(function(){
window.addEventListener('message', function(e) {
alert('data from domain2 sss ---> ' + e.data);
}, false);
},10)
</script>
简单地来说,就是一边使用 postMessage 来发送。
一边通过监听 addEventListener - message 来接收。
通常这里,我们是会判断 origin 的来源的,不然不好区分到底是谁来跨的域。
四、跨域数据安全问题与CSRF攻击
4.1 HTTP是无状态的与Cookie、Session
4.2 Cookie的安全性问题
4.3 为什么Cookie有问题,却没有人提出对规则进行修改
4.4 利用Cookie的问题,进行CSRF攻击
相关阅读
MDN-CORS(跨域资源共享)——[https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS]
知乎文章——九种跨域——[https://zhuanlan.zhihu.com/p/55869398]