CORS(跨域资源共享)浏览器安全机制
在页面中经常会加载一些静态资源例如图片、css、js文件, 这些文件并不都是来自同一个域的。他们并不受浏览器同源策略的限制。浏览器的同源策略限制的是从js脚本里发出的http请求, 例如fetch XMLHttpRequest等,这里的限制并不是不能发出,而是响应的结果会被阻止。除非有正确的响应头。
浏览器的同源策略:只允许协议、域名、端口相同的时候才可以在脚本里发送http请求
实现跨域的方法有很多种:
- JSONP
- WebSocket
- CORS
- postMessage
- document.domain
- window.name
- location.hash
- http-proxy
- nginx
JSONP
jsonp的原理就是因为新建一个script标签,通过这个标签来发送get请求。 因为不是在js脚本内发出的,所以不受浏览器同源策略的限制
优点:兼容性好
缺点:只支持get请求,容易被xss攻击,不安全所以现在基本上都不使用这种方式了function addScriptTag(src){
var script = document.createElement('script');
script.setttribute('type','text/javascript');
script.src = src;
}
window.onload = function(){
addScriptTag('http://example.com/ip?callback=foo');
}
function foo(data){
console.log('your public ip address is'+ data.ip)
}
服务器收到请求后会把data放进foo函数里当做实参,类似与foo({ip:0.0.0.0})
上面的结果会被当做script执行
下面我们来使用promise来封装jsonp <br />
//=> 解构赋值
function jsonp({url, params, cb}) {
return new Promise((resolve, reject) => {
// 手动创建一个 script,插入到 body 后面
let script = document.createElement('script');
// 拼接参数
params = {...params, cb};
let arr = [];
for (let k in params) {
arr.push(`${k}=${params[k]}`);
}
script.src = `${url}?${arr.join('&')}`;
document.body.appendChild(script);
// 创建一个全局 cb 回调函数
// 数据请求回来之后,会执行这个函数,触发 resolve,完成使命,移除这个 script 标签
// 继而执行 then 中定义的回调函数
window[cb] = function (data) {
resolve(data);
document.body.removeChild(script);
}
})
}
如果后端使用node express 那么后端代码怎么写呢
let express = require('express')
let app = express()
app.get('/say',function(){
let {wd,cb} = req.query
res.end(`${cb}(data)`)
})
app.listen(3000)
CORS
CORS 是跨域资源共享的缩写 ,相比与JOSNP只能发送get请求, CORS没可以发起任何类型的请求,
目前主流的浏览器都支持,但是IE10以下的浏览器不支持
整个CORS通信过程都是由浏览器自动完成的,不需要前端代码的参与,对于前端开发这来说CORS和普通的AJAX并没有区别, 代码完全一样,浏览器一旦发现是一个跨源请求就会自动的添加一些头信息,又是还会出现附加一次的请求,但是用户并不会感知。
所以实现CORS的关键是服务器,只要浏览器实现了CORS接口,就可以实现跨源通信。
两种请求
浏览器将CORS请求分为两类,简单请求和非简单请求
只有同时满足以下两种条件就属于简单请求
- 请求方式是GET POST HEAD 中的一种
- 请求头不超过以下几种字段:
- Accept
- Accept-language
- Content-language
- Last-Event-ID
- Content-Type:只限于以下三种字段 application/x-www-form-urlencoded multipart/form-data text/plain
凡是不满足以上两个条件的都属于非简单请求
浏览器对于两种请求方式的处理是不一样的
简单请求
基本流程
对于简单请求,浏览器直接发出CORS请求, 不需要客户端操作,具体来首就是在头信息增加一个Origin字段
GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
以上的信息中,Origin字段用来说明,本次请求来自哪个源,服务器根据这个值来决定是否同意这次请求
如果Origin指定的源头不在服务器的徐可可范围内, 服务器就会返回一个正常的response. 然后浏览器发现这个response头里没有Access-Control-Allow-Origin的字段就知道出错了,从而跑出一个错误, 这个错误就会被XMLHttpRequest的onerror函数捕获。
如果Origin 在服务器的许可范围内,服务器的response就会多出几个响应头
Access-Control-Allow-Origin: http://api.bob.com
Accesss-Control-Allow-Credentials:true
Access-Control-Expose-Headers:FooBar
Content-Type:text/html;charset=utf-8
上面的头信息之中有三个是CORS请求有关,都是以Access-Control开头
Access-Control-Allow-Origin
这个字段是必须的,他要么是请求的Origin字段,要么是一个 , 就便是就收任意域名的请求
Access-Control-Allow-Credentials
这个字段可选, 它的值是一个布尔值 ,表示是否允许发送cookie,
默认情况下, Cookies不包括在CORS中,设置为true,表示服务器明确许可Cookie可以包含在请求中一起发送给服务器,这个值只能设置为true,如果不发送Cookies,删除该字段即可
Access-Control-Expose-Headers
该字段是可选的
CORS请求时,XMLHttpRequest对象的getResponseHeader() 方法值能拿到6个字段,
Cache-Control
Content-language
Content-Type
Expires
Last-Modified
Pragma
如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面制定 多个值需要用逗号隔开
WithCredentials 属性
CORS 请求默认是不发送Cookies 和 HTTP认证信息, 如果要把Cookie发送到服务器,首先要服务器同意,制定Access-Control-Allow-Credentials字段为true
然后开发者必须在AJAX请求中打开withCredentials属性 ,否则浏览器也不会发送。后者服务器要求
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
**
否则 即使服务器同意发送Cookies,浏览器也不会发送, 或者服务器要求设置Cookie,浏览器也不会处理
但是,如果省略withCredentials设置, 有时候浏览器还是会一起发送Cookies. 这时 可以显示关闭withCredentials
xhr.withCredentials = false
需要注意事,如果要发送Cookies. Access-Control-Allow-Origin 就不能设置为*,必须制定明确的的origin,
同时Cookies依然遵循同源策略,只有服务器制定设置的域名才会上传,其他的域名的Cookie并不会上传,并且跨域源网页中英文无法使用document.cookie来读取服务器域名下的Cookie
非简单请求
预检请求
非简单请求是那种对于浏览器有特殊要求的请求。 比如说put delete 或者字段类型是application/json
非简单请求的CORS请求,会在正式通信之前,增加一次http OPTIONS请求, 成为预检 preflight
浏览器先询问服务器,当前页面所载的域名是否在服务器许可的名单之中,以及可以使用那些请求方法和头信息字段。只有达到肯定答复,浏览器才会正式的发出ajax请求,否则报错
下面是浏览器的ajax脚本
var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
上面的代码中 http请求的方法是put 并且发送个自定义的头字段 X-Custom-Header
浏览器发现 这是一个非简单请求 就自动发出一个preflight, 要求服务器确认可以这样发出请求,下面是预请求的头
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
预检的请求方法是OPTIONS, 表示这个请求时用来询问的, 头信息里面,关键字段是Origin,表示请求来自哪个源
除了Origin字段, 预检请求的头信息里面包括两个特殊的字段
Access-COntrol-Request-Method
该字段是必须的 用来列出浏览器的CORS方法会用到哪些http方法
Access-Control-Request-Headers
该字段是一个逗号的分割的字符串,制定浏览器的CORS请求会额外发送的头字段,上面的例子总是X-Custom-header
预检请求的回应
服务器收到预检请求以后,检查了Origin,Access-Control-Request-Method,Access-Control-Request-Headers等字段以后,确认允许跨域请求,就可以做出回应
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
我们可以看到在回应的头字段中,关键的事Access-Control-Allow-Origin, 他表示允许http://api.bob.com这个请求数据了。该字段设置为*, 表示同意任意跨域请求
如果浏览器否定预检请求,会发挥一个正常的http请求,但是没有任务CORS相关的头字段, 这时浏览器就会认定服务器不同意预检请求,因此会触发一个错误信息,被XMLHttpRequest对象的onError函数捕获,控制台就会打印如下信息XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
如果浏览器同意了预检请求, 那么response的头信息除了Access-Control-Allow-Origin就会包括以下字段
Access-Control-Allow-Methods
该字段表示允许浏览器使用的请求方法,包括并且不限于预检请求中的方法
Access-Control-Allow-Headers
这个字段表示服务器所支持的请求头信息, 包括并不限于预检请求中的Access-Control-Request-Headers中的字段
Access-Control-Allow-Credentials
该字段预简单请求时的含义相同
Access-Control-Max-Age
该字段可选,用来指定本次预检请求的有效期,单位是秒,在此期间不用发送另一条预检请求
浏览器的正常请求和回应
一旦预检请求通过, 浏览器就会发送正常的CORS请求, 就是跟简单的请求一样,会有一个Origin头字段,服务器的回一个也会有个Access-Control-Allow-Origin头字段信息
下面的预检请求之后,浏览器发出的正常的CORS请求
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
头信息的Origin字段是浏览器自动添加的
下面是服务器的正常回应 Access-Control-Allow-Origin: [http://api.bob.com](http://api.bob.com)
Content-Type: text/html; charset=utf-8
上面的头信息中,Access-Control-Allow-Origin 字段是每次回应都必须包含的
下面是代码
let xhr = new XMLHttpRequest();
document.cookie = 'name=aa';
xhr.withCredentials = true;
xhr.open('put', 'http://localhost:4000/getData');
xhr.setRequestHeader('name', 'aa');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
console.log(xhr.responseText);
console.log(xhr.getResponseHeader('name'));
}
}
}
xhr.send();
服务端代码
let express = require('express');
let app = express();
// 建立可以访问白名单
let whiteList = ['http://localhost:3000'];
app.use(function (req, res, next) {
let origin = req.headers.origin;
if (whiteList.includes(origin)) {
//=> 允许哪个源可以访问,如果是 '*' 号,那么就所有都可以访问,但是不能与携带 cookie 一起使用
res.setHeader('Access-Control-Allow-Origin', origin);
//=> 允许携带哪个头访问我,多个则在字符串拼接即 'name, age'
res.setHeader('Access-Control-Allow-Headers', 'name');
//=> 允许哪个方法访问,get 方法不设置也可以
res.setHeader('Access-Control-Allow-Methods', 'PUT');
//=> 预检的存活时间,设置 options 探测请求的间隔时间,减少 options 请求的发送
res.setHeader('Access-Control-Max-Age', 6);
//=> 允许携带 cookies
res.setHeader('Access-Control-Allow-Credentials', true);
//=> 设置哪个头信息是安全的,客户端可以获取的
res.setHeader('Access-Control-Expose-Headers', 'name');
if (req.method === 'OPTIONS') {
res.end(); //=> 会有一个试探的 options 请求,直接结束即可
}
}
next();
})
app.get('/getData', function (req, res) {
console.log(req.headers);
res.end("中文");
});
app.put('/getData', function (req, res) {
console.log(req.headers);
res.setHeader('name','aa');
res.end("中文");
});
//=> 以当前目录作为静态文件目录
app.use(express.static(__dirname));
app.listen(4000);
和JSONP比较的话
- JSONP的兼容性比较好,不仅兼容所有的浏览器,而且还可以像不支持CORS的服务发起请求,但是只能发送GET请求
- CORS支持所有类型的请求,但是兼容性不太好 IE10以下的不支持
postMessage
a 页面可以通过iframe 和 postMessage 向b 页面发送消息,其中a,b不在同一个域下
<iframe src="http://localhost:4000/b.html"
frameborder="0"
id="frame"
onload="load()">
</iframe>
<script>
function load() {
let frame = document.getElementById('frame');
//=> 接受两个参数,数据以及传递的地址
frame.contentWindow.postMessage('aa', 'http://localhost:4000/b.html');
}
window.onmessage = function (e) {
console.log(e.data);
}
</script>
当iframe加载完毕以后向b也买呢发送一个消息,此时需要通过一个frame.contentWindow 获取b页面,使用启postMessage方法
<script>
window.onmessage = function (e) {
console.log(e.data);
e.source.postMessage('bb', e.origin);
}
</script>
b页面通过window.onmessage 监听到这个发送过来的数据,通过e.data获取数据
然后b页面获取到数据之后,通过e.source获取a页面,通过其postMessage方法向a页面发送消息
a页面中也是通过window.onmessage来监听发过来的数据 通过e.data拿到数据
另外比较奇怪的是 我测试中接受到两次b页面发过来的数据,第二次为空
document.domain
用于解决一级域名和二级域名之间的跨域问题,只需要连个也买呢的document.domain都设置为定级域名即可
思路:
- 假设a页面是http://a.baidu.com:3000 b页面是http://b.baidu.com:3000
- a页面没办法直接通过 iframe.contentWindow.a 获取b页面window中的a属性
- 两个也买呢都设置document.domain 为http://baidu.com即可
<iframe src="http://b.destiny.cn:3000/b.html"
frameborder="0"
onload="load()"
id="iframe">
</iframe>
<script>
document.domain = 'destiny.cn';
function load() {
console.log(iframe.contentWindow.a);
}
</script>
记住定义 a 的时候要使用var 不要使用let 因为let不会挂在到window上<script>
document.domain = 'destiny.cn';
var a = 100;
</script>
如果要模拟一级域名 二级域名可以设置host
127.0.0.1 b.baidu.com
127.0.0.1 a.baiud.com