为什么会存在同源策略
概念
1995年有Netscape公司引入的一个安全策略,现在所有浏览器都在使用这个策略。它限制了一个源从另外一个源请求资源,用于隔离潜在恶意文件。
什么才是不同的源
我们都知道一般的地址又下面三部分组成
- 协议相同
- 域名相同
- 端口相同
只要两个地址其中有一个不相同,那么这两个地址就是不同的源。
举个例子
//地址
http://www.address.com/item/page.html
协议: http://
域名:www.example.com
端口:8080(http)/443(https) (端口默认省略)
http://www.address.com/item2/other.html:同源
http://address.com/item/other.html:不同源(域名不同)
http://v2.www.address.com/item/other.html:不同源(域名不同)
http://www.address.com:81/item/other.html:不同源(端口不同)
同源的目的
目的是为了保护用户信息的安全,防止恶意网站窃取数据,否则Cookie可以共享。有的网站一般会把一些重要信息存放在cookie或者LocalStorage中,这时如果别的网站能够获取获取到这个数据,可想而知,这样就没有什么安全可言了。
限制范围
目前共有三种行为受到限制
- Cookie、LocalStorage和IndexDB 无法读取
- DOM无法获得
- AJAX 请求不能发送
解决方案
1、通过jsonp跨域
原理
script、img、iframe等标签的src属性都拥有跨域请求资源的能力,我们可以把js、css、img等资源放到一个独立域名的服务器上。然后通过动态创建script标签,再请求一个回调函数的网址,服务器把需要传递的参数塞入这个回调函数中, 然后我们在js代码中执行这个函数就可已获取到你想要的参数。
前端原生实现
<script>
window.xxx = function (value) {
console.log(value)
}
var script = document.createElement('script')
script.src = 'https://www.address.com:433/json?callback=xxx'
document.body.appendChild(script)
</script>
jquery实现
$.ajax({
type: "get",
url: "https://www.address.com:433/json",
dataType: "jsonp",
jsonp: "callback",
jsonpCallback:"xxx"//自定义回调函数名
success: function(res){
console.log(res)
},
error: function(){
console.log('fail');
}
});
jsonp插件
//安装
npm install jsonp
const jsonp = require('jsonp');
jsonp('https://www.address.com:433/json', {parma:'xxx'}, (err, data) => {
if (err) {
console.error(err.message);
} else {
console.log(data);
}
});
node.js egg 服务端
//需要看egg官网构建一个simple框架
egg-init --type=simple
// router.js 用egg内置jspnp方法 https://eggjs.org/api/Config.html#jsonp
module.exports = app => {
app.get('/json', app.jsonp({ callback: 'xxx' }), app.controller.json.index)
}
缺点
- 只支持GET请求,不支持POST请求
- 调用失败没有HTTP状态码
- 不够安全。
2、CORS
原理
CORS(Cross-Origin Resource Sharing)跨资源分享,浏览器不能低于IE10,服务器支持任何类型的请求。
熟悉几个后台需要设置的字段
- Access-Control-Allow-Origin
字段必传,为*表示允许任意域名的请求。当有cookie需要传递时,需要指定域名。
- Access-Control-Allow-Credentials
字段可选,默认为false,表示是否允许发送cookie。若允许,通知浏览器也要开启cookie值的传递。
- Access-Control-Expose-Headers
字段可选。如果想要浏览器拿到getResponesHeader()其他字段,就在这里指定。
- Access-Control-Request-Method
必须字段,非简单请求时设置的字段,例如PUT请求。
- Access-Control-Request-Headers
指定额外的发送头信息,以逗号分割字符串。
前端原生代码
var xhr = new XMLHttpRequest()
// 设置携带cookie
xhr.withCredentials = true;
xhr.open('POST', 'https://www.address.com:433/json', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
xhr.send(null)
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(JSON.parse(xhr.responseText).msg)
}
}
后端代码
module.exports = app => {
class CrosController extends app.Controller {
* index(req) {
this.ctx.set('Access-Control-Allow-Origin', 'https://www.address.com');
this.ctx.set('Access-Control-Allow-Credentials', 'true')
this.ctx.body = { msg: 'hello world' }
}
}
return CrosController
}
优点
- 支持所有类型的HTTP请求。
缺点
- 不兼容IE10以下。
3、iframe结合locaction.hash方法
原理
也是利用iframe可以在不同域中传值的特点,而location.hash正好可以携带参数,所以利用iframe作为这个不同域之间的桥梁。
具体实现步骤
- 1、向A域名页面插入一个B域名的iframe标签。
- 2、然后在B域名的iframe页面中ajax请求同域名的服务器。
- 3、iframe页面拿到数据后通过praent.location.href将想要传递的参数放到#后面,作为hash传递。
- 4、这样在A域名页面就能通过window.onhashchange 监听处理你想要的数据。
A域名页面
var iframe = document.createElement('iframe')
iframe.src = 'http://www.B.com:80/hash.html'
document.body.appendChild(iframe)
window.onhashchange = function () {
//处理hash
console.log(location.hash)
}
B域名页面
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
var res = JSON.parse(xhr.responseText)
console.log(res.msg)
parent.location.href = `http://www.A.com:80/a.html#msg=${res.msg}`
}
}
xhr.open('GET', 'http://www.B.com:80/json', true)
xhr.send(null)
缺点
- iframe虽然能解决问题,但是安全风险还是比较重要的。
- hash传参处理起来比较麻烦。
4、iframe结合window.name方法
原理
原理其实是和上面的方法一样,区别在于window.name能够传递2MB以上的数据。
A域名页面
var iframe = document.createElement('iframe')
iframe.src = 'http://www.B.com:80/name.html'
document.body.appendChild(iframe)
var times = 0
iframe.onload = function () {
if (times === 1) {
console.log(JSON.parse(iframe.contentWindow.name))
destoryFrame()
} else if (times === 0) {
times = 1
}
}
// 获取数据以后销毁这个iframe,释放内存;
function destoryFrame() {
document.body.removeChild(iframe);
}
B域名页面
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
window.name = xhr.responseText
location.href = 'http://www.A.com:80/a.html'
}
}
xhr.open('GET', 'http://www.B.com:80/json', true)
xhr.send(null)
5、postMessage跨域
原理
postMessage是H5原生API支持,可以在两个页面或者多个页面,以及不同源页面之间传递数据。窗口之间能够传递数据的前提是必须从一个窗口获取到另外一个窗口的目标对象(target window),比如说用iframe打开另外一个窗口,我们得获取到这个iframe的contentWindow。或者通过window.open()打开另外一个窗口时会返回这个窗口的window对象。
参数传递
/**
data 需要传递的数据,使用JSON.stringify 序列化
origin 设置为'*'时,表示传递给所有窗口,也可以指定地址。 如果是同源下设置为'/'
**/
postMessage(data,origin)
// 打开一个新的窗口
var popup = window.open('http://localhost:8080');
/// 等待新窗口加载完
setTimeout(function() {
// 当前窗口向目标源传数据
popup.postMessage({"age":10}, 'http://localhost:8080');
}, 1000);
// 设置监听,如果有数据传过来,则打印
window.addEventListener('message', function(e) {
console.log(e);
//判断是不是目标地址
if(e.origin !== 'http://localhost:8069')return;
// console.log(e.source === window.opener); // true
//发回数据
e.source.postMessage({"age":20}, e.origin);
});
其他的解决方式
以上五种是比较常见的跨域解决方案,各有优缺点。没有绝对的最优方案,只有最合适应用场景。
常见的两种代理跨域
- nginx反向代理跨域 node中间件代理
配置就不讲了,其实我也不熟悉,平时工作用不到这么高大上的。就来讲讲原理以及思路。
什么是代理
既然是代理跨域,那么代理(Proxy Server)就是一个很重要的点,这里的代理说的服务器代理,是一种很重要的服务器安全功能,也是一种很常见的设计模式,来隔绝不同的模块,解耦模块。生活中也很容易见到这种模式,我们买房(买不起呀)买车,就好比跟一个大型服务器打交道,别人是大老板,自然不会那么容易亲自接待你,这时候有中介在中间,帮你们两方理清好思路,做好铺垫,然后后面的交流才会更加顺通高效。我们浏览器访问不同源的服务器是有限定的,但是nginx和node中间件这些代理就没有跨域的限定,所以我们可以放心的把任务交给他们,让他们帮我们去做我们做不了的事。
为什么代理是反的
我们知道单个服务器的处理能力是有限的,就好比如中国也不可能只有一家汽车生产商,中国那么大的市场,自然需要很多生产商才能满足的过来。那么用户选购的时候需要节省时间和精力,汽车之间就承担这样一个角色,把成千上万的用户需求分配出去,nginx就能够把用户的请求分发到空闲的服务器上,然后服务器返回自己的服务到负载均衡设备上,然后负载均衡的设备会讲服务器的服务返回给用户,所以我们并不知道为什么服务的是哪一台服务器发送出来的,这就很好的隐藏了服务器。有一句精辟的话是这么说的:“反向代理就是流量发散状,代理是流量汇聚状。”
最后附上一张画的很丑陋的图
如果大神您想继续探讨或者学习更多知识,欢迎加入QQ或者微信一起探讨:854280588