开发中一定会碰到的问题,前后端数据交互,请求跨域,然后请求被拦截。那么为什么会被拦截呢?下面就让我们一起来揭开浏览器同源策略的这层面纱。
一、跨域
跨域形成的原因是:违背了浏览器的同源策略,所以浏览器出于保护,会拦截请求。
二、同源策略
同源策略是一种约定,它是浏览器最核心也是最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS,CSRF等攻击。所谓源(origin)其实就是URL,指得就是:协议+域名+端口,所以即便两个不同的域名指向同一个ip地址,也非同源。
URL解析:
因此同源策略通俗的将就是:浏览器出于对网站安全性的考虑,限制了不同源之间资源相互访问的一种政策。
同源策略限制的内容:
- Cookie、LocalStorage、IndexedDB(是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs)) 等存储性内容
- 无法获取DOM元素并进行操作。
- AJAX 请求发送后,结果被浏览器拦截了
但是有三个标签是允许跨域加载资源:
- <img src=XXX>
- <link href=XXX>
- <script src=XXX>
三、常见跨域场景
当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作“跨域”。常见跨域场景如下图所示:
特别说明两点:
第一:如果是协议和端口造成的跨域问题“前台”是无能为力的。
第二:在跨域问题上,仅仅是通过“URL的首部”来识别而不会根据域名对应的IP地址是否相同来判断。“URL的首部”可以理解为“协议, 域名和端口必须匹配”。
那么这里我们是否有一个疑惑,请求跨域了,那么请求到底发送出去了吗?
首先我们要确定的是:跨域并不是请求发不出去,请求能发出去,服务端也能接收到并且正常返回数据,其次就是返回的数据是被浏览器拦截了。
四、跨域的解决方案
1、jsonp
jsonp是跨域领域中历史非常传统的一种方法。上面 我们有提到一些标签是不受跨域限制的,这里使用的就是script标签,
实现流程:
- 声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。
- 创建一个
<script>
标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show)。 - 服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是show,它准备好的数据是
show('我不爱你')
。 - 最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作。
在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要自己封装一个 JSONP函数。
// index.html
function jsonp({ url, params, callback }) {
return new Promise((resolve, reject) => {
let script = document.createElement('script')
window[callback] = function(data) {
resolve(data)
document.body.removeChild(script)
}
params = { ...params, callback } // wd=b&callback=show
let arrs = []
for (let key in params) {
arrs.push(`${key}=${params[key]}`)
}
script.src = `${url}?${arrs.join('&')}`
document.body.appendChild(script)
})
}
jsonp({
url: 'http://localhost:3000/say',
params: { wd: 'Iloveyou' },
callback: 'show'
}).then(data => {
console.log(data)
})
// server.js
let express = require('express')
let app = express()
app.get('/say', function(req, res) {
let { wd, callback } = req.query
console.log(wd) // Iloveyou
console.log(callback) // show
res.end(`${callback}('我不爱你')`)
})
app.listen(3000)
当然,如果你是用类似jquery这样的库,其中的$.ajax
本身是封装了JSONP方式的:
$.ajax({
url: 'http://127.0.0.1:3000/info/jsonp?cb=myCallback',
dataType: 'jsonp', // 注意,此处dataType的值表示请求使用JSONP
jsonp: 'cb', // 请求query中callback函数的键名
}).done(function (res) {
alert(JSON.stringify(res, null , 2));
});
JSONP作为一个久远的方法,其最大的优点就是兼容性非常好。
缺点:由于是通过script标签发送的请求,所以只支持get请求,并且前后端的开发改造要多一点,如果跨域服务端不支持改造,那么也无法使用该方法,限制因素很多。
2、CORS
CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。
浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求。
简单请求:
只要同时满足下面的两大条件,就属于简单请求:
条件一:
- GET
- HEAD
- POST
条件二:Content-Type 的值仅限于下列三者之一
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器; XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。
复杂请求:
不符合以上条件的请求就肯定是复杂请求了。 复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。
下面的详细介绍就不在一一赘述了,想要了解子段信息或者查看代码块可以参考下面的链接:
九种跨域方式实现原理(完整版)juejin.im3、proxy代理
这种方式仍然遵守同源策略,只是转换了一种思路,将请求移至到了服务端。
我们知道,同源政策是浏览器层面的限制。那么,如果我们不在前端跨域,而将“跨域”的任务交给后端服务,是否就规避了同源政策呢?是的。
这就是“代理”。这个代理可以将我们的请求转发,而后端并不会有所谓的同源政策限制。这个“代理”也可以理解为一个同域的后端服务。
优点:不需要第三方服务做任何的修改,只需要在前端的项目中配置proxy代理即可。
缺点:需要你有一个自己的后端服务能够接收并转发请求。如果你进行本地的纯静态页面开发,则需要一些浏览器插件或自动化工具中集成的本地服务器来实现。此外,如果请求包含一些特殊的请求头(例如cookie等等),需要在转发时特殊处理。
到这里就讲述完毕了,总结一下:
- 讲述了跨域的原因==>同源策略==>浏览器出于对网站的保护机制==>同源指的是:协议+域名+端口号 都必须相同
- 跨域的解决方案:
- jsonp最久远的方法,
- 优点:兼容性好,
- 缺点:因为利用的是script标签,所以只支持get,并且前后端开发改造很多,限制性太强
- cors:
- 优点:简单,只需要简单配置相应的请求与响应头信息即可,服务端需要配置Access-Control-Allow-Origin这个字段表明接收那些请求,支持各种类型的请求(
get
,post
,put
等等) - 缺点:需要对跨域的服务接口进行一定的改造。如果该服务因为某些原因无法改造,则无法实现。不兼容一些“古董”浏览器。
- 优点:简单,只需要简单配置相应的请求与响应头信息即可,服务端需要配置Access-Control-Allow-Origin这个字段表明接收那些请求,支持各种类型的请求(
- proxy代理:
- 优点:简单,服务端不需要修改配置,前端只需配置proxy代理即可,
- 缺点:需要有一个自己的后端服务接收并转发请求,对于一些特殊的请求头,需要做特殊处理。
- jsonp最久远的方法,
参考链接:
浏览器同源策略与ajax跨域方法汇总www.jianshu.com