为什么会存在同源策略
概念
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)
http://localhost:8069 窗口
// 打开一个新的窗口
var popup = window.open(‘http://localhost:8080’);
/// 等待新窗口加载完
setTimeout(function() {
// 当前窗口向目标源传数据
popup.postMessage({“age”:10}, ‘http://localhost:8080’);
}, 1000);
http://localhost:8080 窗口
// 设置监听,如果有数据传过来,则打印
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