什么是同源策略?
如果两个页面的协议,端口(如果有指定)和主机都相同,则两个页面具有相同的源。
同源策略是一种安全协议 ,它的协议,域名,端口相同
下表给出了相对http://store.company.com/dir/page.html
同源检测的示例:
URL | 结果 | 原因 |
---|---|---|
http://store.company.com/dir2/other.html | 成功 | 只有路径不同 |
http://store.company.com/dir/inner/another.html | 成功 | 只有路径不同 |
https://store.company.com/secure.html | 失败 | 不同协议 ( https和http ) |
http://store.company.com:81/dir/etc.html | 失败 | 不同端口 ( http:// 80是默认的) |
http://news.company.com/dir/other.html | 失败 | 不同域名 ( news和store ) |
跨域的概念
也就是说,如果协议、域名、端口有一个不同就是跨域,Ajax请求失败。
为什么会有跨域这个问题出现
因为浏览器出于安全考虑,有同源策略。因为浏览器的同源策略导致了跨域,就是浏览器在搞事情。
浏览器为什么要搞事情?
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。
没有同源策略限制的两大危险
浏览器是从两个方面去做这个同源策略的,一是针对接口的请求,二是针对Dom的查询。试想一下没有这样的限制上述两种动作有什么危险。
-
没有同源策略限制的接口请求
有一个小小的东西叫cookie大家应该知道,一般用来处理登录等场景,目的是让服务端知道谁发出的这次请求。如果你请求了接口进行登录,服务端验证通过后会在响应头加入Set-Cookie字段,然后下次再发请求的时候,浏览器会自动将cookie附加在HTTP请求的头字段Cookie中,服务端就能知道这个用户已经登录过了。知道这个之后,我们来看场景:
- 你准备去清空你的购物车,于是打开了买买买网站www.maimaimai.com,然后登录成功,一看,购物车东西这么少,不行,还得买多点。
- 你在看有什么东西买的过程中,你的好基友发给你一个链接www.nidongde.com,一脸yin笑地跟你说:“你懂的”,你毫不犹豫打开了。
- 你饶有兴致地浏览着www.nidongde.com,谁知这个网站暗地里做了些不可描述的事情!由于没有同源策略的限制,它向www.maimaimai.com发起了请求!聪明的你一定想到上面的话“服务端验证通过后会在响应头加入Set-Cookie字段,然后下次再发请求的时候,浏览器会自动将cookie附加在HTTP请求的头字段Cookie中”,这样一来,这个不法网站就相当于登录了你的账号,可以为所欲为了!如果这不是一个买买买账号,而是你的银行账号,那……
-
没有同源策略限制的Dom查询
- 有一天你刚睡醒,收到一封邮件,说是你的银行账号有风险,赶紧点进www.yinghang.com改密码。你赶紧点进去,还是熟悉的银行登录界面,你果断输入你的账号密码,登录进去看看钱有没有少了。
- 睡眼朦胧的你没看清楚,平时访问的银行网站是www.yinhang.com,而现在访问的是www.yinghang.com,这个钓鱼网站做了什么呢?
// HTML <iframe name="yinhang" src="www.yinhang.com"></iframe> // JS // 由于没有同源策略的限制,钓鱼网站可以直接拿到别的网站的Dom const iframe = window.frames['yinhang'] const node = iframe.document.getElementById('你输入账号密码的Input') console.log(`拿到了这个${node},我还拿不到你刚刚输入的账号密码吗`)
补充知识点:窗口关系及框架
红宝书上p194-p196
如果页面中包含框架,则每个框架都拥有自己的window对象 ,并且保存在frames集合中,在frames集合中,可以通过数值索引 (从0开始,从左至右,从上到下)或者框架名称来访问相应的window对象。每个window对象都有一个name属性,其中包含框架的名称。
解决方案
同源策略是浏览器做的一件好事,是用来防御来自邪门歪道的攻击,但总不能为了不让坏人进门而把全部人都拒之门外 。没错,我们这种正人君子只要打开方式正确,就应该可以跨域。
同源策略限制下接口请求的正确打开方式:
- JSONP
借用script、 img标签没有跨域限制的漏洞,通过这个标签指向一个需要访问的地址并提供一个回调函数来接收数据当需要通讯时。他使用简单,且兼容性不错,但是只限于get请求。
/**
* JSONP请求工具
* @param url 请求的地址
* @param data 请求的参数
* @returns {Promise<any>}
*/
const request = ({url, data}) => {
return new Promise((resolve, reject) => {
// 处理传参成xx=yy&aa=bb的形式
const handleData = (data) => {
const keys = Object.keys(data)
const keysLen = keys.length
return keys.reduce((pre, cur, index) => {
const value = data[cur]
const flag = index !== keysLen - 1 ? '&' : ''
return `${pre}${cur}=${value}${flag}`
}, '')
}
// 动态创建script标签
const script = document.createElement('script')
// 接口返回的数据获取
window.jsonpCb = (res) => {
document.body.removeChild(script)
delete window.jsonpCb
resolve(res)
}
script.src = `${url}?${handleData(data)}&cb=jsonpCb`
document.body.appendChild(script)
})
}
// 使用方式
request({
url: 'http://localhost:9871/api/jsonp',
data: {
// 传参
msg: 'helloJsonp'
}
}).then(res => {
console.log(res)
})
- 空iframe加form
细心的朋友可能发现,JSONP只能发GET请求,因为本质上script加载资源就是GET,那么如果要发POST请求怎么办呢? 就用这种方式;
-
CORS(常用)
CORS:“跨域资源共享”(Cross-origin resource sharing);
CORS有两种请求,简单请求和非简单请求。
- 简单请求要满足以下两大条件:
(1) 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
(2)HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
2.非简单请求
非简单请求会发出一次预检测请求,返回码是204,预检测通过才会真正发出请求,这才返回200。这里通过前端发请求的时候增加一个额外的headers来触发非简单请求。
简单请求:
前端什么也不用干,就是正常发请求就可以,如果需要带cookie的话,前后端都要设置一下,下面那个非简单请求例子会看到。
非简单请求:
后端: 如果需要http请求中带上cookie,需要前后端都设置credentials,且后端设置指定的origin,非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight),这种情况下除了设置origin,还需要设置Access-Control-Request-Method以及Access-Control-Request-Headers;credentials: ‘include’,
前端需要带上cookie,要加上
- 代理(Nginx)
如果我们请求的时候还是用前端的域名,然后有个东西帮我们把这个请求转发到真正的后端域名上,不就避免跨域了吗?这时候,Nginx出场了
请求的时候直接用回前端这边的域名http://localhost:9099,这就不会跨域,然后Nginx监听到凡是localhost:9099/api这个样子的,都转发到真正的服务端地址http://localhost:9871
Nginx转发的方式似乎很方便!但这种使用也是看场景的,如果后端接口是一个公共的API,比如一些公共服务获取天气什么的,前端调用的时候总不能让运维去配置一下Nginx,如果兼容性没问题(IE 10或者以上),CROS才是更通用的做法吧。
同源策略限制下Dom查询的正确打开方式
- document.domain
该方式只能用于二级域名相同的情况下,比如
a.test.com
和b.test.com
适用于该方式。(知识点:a是三级域名,test是二级域名,com是顶级域名)只需要给两个页面添加
document.domain = 'test.com'
表示二级域名都相同就可以实现跨域
- postMessage
window.postMessage() 是HTML5的一个接口,专注实现不同窗口不同页面的跨域通讯。 一个页面发送消息,另一个页面判断来源并接收消息。