为什么会有跨域?
出于安全性考虑
,浏览器限制脚本内发起的跨源HTTP请求。例如,XMLHttpRequest和Fetch API遵循同源策略
。这意味着使用这些API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源,除非响应报文包含了正确CORS响应头。
同源策略:如果两个URL的protocol(协议)、port(端口,如果有指定的话)和host(域名)都相同的话,则这两个URL是同源。
跨域解决方案
1. JSONP
在CORS之前,开发人员也有跨域请求资源的需求,他们提出了多种方案,其中JSONP为常见的一种。JSONP作为一种古老的方案已经不被推荐在项目中使用,但其优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据,我们也有必要了解下其实现原理,开阔开发思路。
原理: 所有具有src属性的HTML标签都是可以跨域的。在浏览器中,<script>、<img>、<iframe>和<link>这几个标签是可以加载跨域(非同源)的资源的,并且加载的方式其实相当于一次普通的GET请求,唯一不同的是,为了安全起见,浏览器不允许这种方式下对加载到的资源的读写操作,而只能使用标签本身应当具备的能力(比如脚本执行、样式应用等等)。
JSONP的缺点:
- 只能实现get一种请求方式。
- JSONP在调用失败的时候前端不能获取各种HTTP状态码。
- 安全性问题。万一假如提供JSONP的服务存在页面注入漏洞,即它返回的javascript的内容被人控制的。那么结果是什么?所有调用这个JSONP的网站都会存在漏洞。于是无法把危险控制在一个域名下…所以在使用JSONP的时候必须要保证使用的JSONP服务必须是安全可信的。
利用script标签天生具有跨域能力和脚本执行的能力的原理,前后端约定好一个函数名callbackName,使用script标签发送请求,服务器端获取数据res后响应请求返回callback(res)格式的脚本,则会在客户端将res作为参数执行函数callback,至此,客户端获取到了接口的返回值res。
先实现一个简单的JSONP。
jsonp_v1版本
服务器示例代码(node) http://xxx.xxx.com
router.get('/test', async ctx => {
const { keyword, callback } = ctx.query;
let res = await testController.getData({ keyword });
ctx.body = `${callback}({
code: 1,
data: ${JSON.stringify(res)},
msg: '请求成功'
})`;
});
前端示例代码
// 先处理下参数
const getEncodeParams = (data = {}) => {
let res = []
for (let key in data) {
res.push(`${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`);
}
return res.join('&');
}
function jsonp_v1 (url, data, callbackFunc) {
let elem = document.createElement('script');
elem.type = 'text/javascript';
elem.src = `${url}?${getEncodeParams(data)}&callback=callbackFunc`;
document.body.appendChild(elem);
window.callbackFunc = callbackFunc;
}
// 发送请求
jsonp_v1('http://localhost:3000/tool/test', { keyword: 'hello-world' }, function (res) { // 服务器处理请求后会返回"调用这个函数的脚本"
alert(res)
});
缺点:发送多个请求时,window.callbackFunc会被重写,例如运行以下代码,我们期望alert一次,console一次,结果却是console两次。
jsonp_v1('http://localhost:3000/tool/test', { keyword: 'hello-world' }, function (res) { // 服务器处理请求后会返回"调用这个函数的脚本"
alert(res)
});
jsonp_v1('http://localhost:3000/tool/test', { keyword: 'hello' }, function (res) { // 服务器处理请求后会返回"调用这个函数的脚本"
console.log(res)
});