本篇会解释下面这些问题:
什么是源,什么是同源策略,这是个老话题了,简单过下。
Web 内容的源由用于访问它的 URL 的方案(协议)、主机名(域名)和端口定义。只有当协议、主机和端口都匹配时,两个对象才具有相同的源。
总结:只要两个URL的协议、域名和端口号都相同,就是同源;反之,则不是。
浏览器为何要进行同源限制
为了安全,防止跨域访问时所带来的恶意攻击。这里提供一个漏洞练习的靶场,可以玩一下XSS , XSRF
。
先简单介绍下基础概念:
浏览器会将请求分为两类
这两个概念可以参考:
https://juejin.cn/post/7206264862657445947#commenthttps://github.com/amandakelake/blog/issues/62
至于为何要这么分类,可以参考分类原因
这里做个总结:
简单请求就是普通的get , post , head
请求,没有额外的请求头。其他请求就都是非简单请求。
对简单请求,不会做预检请求。而对非简单请求,会做预检请求。也是为了安全考虑。
简单来说,预检请求就是在实际请求之前,浏览器先使用OPTIONS
请求方式进行一次请求,为了验证服务器是否允许本次请求。如果允许,就继续完成实际的请求;如果不允许,浏览器就会拦截此次的实际请求。
具体是怎么做的,我们眼见为实,来实际操作一下。这里我们起两个服务器,前端使用Live Server
,后端使用koa
。
前端地址:http://127.0.0.1:5500/index.html
后端地址:http://localhost:4001/
先来看简单请求,前端代码
// index.htmlconst xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:4001/string');
xhr.send();
xhr.onreadystatechange = function(e) {if(e.target.readyState === 4 && e.target.status === 200) {console.log(e.target.response);}
}
可以看到,前端发送了跨域请求
点进去可以看到
后端:
可以看到状态码是200
,说明我们的请求是成功的,但是并没有数据返回,这是怎么回事儿呢?
实际过程是,在发送简单请求时,我们的请求是正常发出的,浏览器会通过cors
带上Origin
请求头,服务端会收到请求,把数据响应给浏览器。浏览器收到响应后,会检查响应头中是否有Access-Control-Allow-Origin
字段,该字段值和请求头的Origin
是否匹配。如果匹配,则浏览器不会拦截数据,我们就能拿到响应数据;如果不匹配,浏览器就会将数据拦截。
我们在后端配置cors
跨域再重新请求,可以看到我们正常发送了一次请求,类型是xhr
,
点进去可以看到请求头的Origin
字段和响应头的Access-Control-Allow-Origin
的值是相同的
这样,前端就获取到了想要的数据
再来看看非简单请求和预检请求是怎么回事儿。我们以DELETE
来触发预检请求,先让后端不允许跨域。
前端代码
const xhr = new XMLHttpRequest();
xhr.open('DELETE', 'http://localhost:4001/test');
xhr.send();
xhr.onreadystatechange = function(e) {if(e.target.readyState === 4 && e.target.status === 200) {console.log(e.target.response);}
}
后端代码:
router.delete('/test', async (ctx, next) => {ctx.body = '已删除'
})
此时,我们可以看到,虽然我们只发了一次请求,但是实际却请求了两次
第一次请求类型为preflight
,即预检请求,状态码为404
。第二次请求是我们的xhr
,状态为跨域。
我们先看看预检请求做了什么
预检的请求方法是OPTIONS
,而把我们实际的请求方法DELETE
作为Access-Control-Request-Method
的值。
再看后端的反应
后端给出了404
,这过程中发生了什么
如果是非简单请求,浏览器会进行预检请求,请求方式为OPTIONS
,向后端发送一次请求,来验证当前的跨域请求是否被服务器允许,如果不允许,浏览器就会拦截,让我们无法把请求发送给后端。所以这时候,后端并不会收到我们的DELETE
请求。下图是我们的DELETE
请求,什么请求也没有。
这时候,我们把后端设置cors
允许前端的请求。我们的预检请求就会通过
之后的DELETE
请求就会收到正常的响应结果,而且此时,后端收到的不是OPTIONS
请求,而是我们的DELETE
请求
当跨域请求可以正常响应时,浏览器依然会发送预检请求,但是当服务器同意我们的DELETE
请求时,服务器就会按照预检请求中的Access-Control-Request-Method
值作为正式请求方法,把结果响应给前端。所以我们在后端看到的是只有DELETE
。
总结:对于简单请求,浏览器是直接放行请求的,如果后端也会正常返回结果给浏览器。如果服务器并没有允许跨域请求,浏览器就会拦截此次响应结果,不返回给前端。
对于非简单请求,浏览器会先进行预检请求。如果后端不允许跨域请求,后端会收到OPTIONS
请求,但并不会把结果返回给浏览器。如果后端允许跨域请求,后端会将Access-Control-Request-Method
的值作为实际请求方式,把结果返回给浏览器。前端会显示两次请求,但是服务器始终只需进行一次响应。
跨域实现方法中,有一种方法叫做代理服务器
实现跨域。原理就是服务器没有同源策略限制。如果前端想要获取服务器B中的资源,那么我们就建立一个服务器A,让前端向服务器A发送请求,A会把本次请求通过代理转接到服务器B上,前端就可以获取到服务器B的资源了。
具体如何实现呢,本次以koa为例。我们用koa再创建一个服务器B,地址为
http://localhost:3002
为了区分,我们把之前创建的http://localhost:4001
服务器就成为服务器A。
先在B中写入创建一个接口
router.get('/resource', async (ctx, next) => {ctx.body = '呐!这是你要的资源!';
})
此时,直接向B发送请求,会报跨域错误
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:3002/resource');
xhr.send();
xhr.onreadystatechange = function(e) {if(e.target.readyState === 4 && e.target.status === 200) {console.log(e.target.response);}
}
现在我们来配置服务器A,让A来做代理服务器。首先,需要A需要打开跨域请求,允许前端向A发出请求。然后A需要将请求目标地址代理到B。下面直接看做法
// 在A中安装插件// 跨域插件
npm i koa2-cors
// 这里我们使用的是express的跨域插件,因为这个好用
npm i http-proxy-middleware
// koa不能直接使用express的插件,需要使用connect转接一下
npm i koa2-connect
然后在app.js
中做如下配置:
app.use(cors());// https://github.com/chimurai/http-proxy-middleware
app.use(connect(// 代理全部以 / 开头的 HTTP 请求createProxyMiddleware('/', {target: 'http://localhost:3002', // 目标服务器B的地址changeOrigin: true // 允许跨域}))
);
现在,让前端向A发送请求
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:4001/resource');
xhr.send();
xhr.onreadystatechange = function(e) {if(e.target.readyState === 4 && e.target.status === 200) {console.log(e.target.response);}
}
在浏览器中查看
成功获取到服务器B中的响应。