三种跨域方案(jsonp、cors、form+iframe)

必备知识

  1. 同源策略(Same origin policy),浏览器的一个安全策略,现代浏览器大部分都实现了该策略。
    1. 同源指:域名、协议、端口相同。
    2. 禁止使用 XHR 对象向不同源的服务器地址发起 HTTP 请求。
    3. 禁止对不同源页面 DOM 进行操作。这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的。
  2. http协议的Content-Type字段:如果该字段在http request header(请求头)里,指“前端向后台传递的数据的类型”,后台框架可能会根据该字段,自动地对前端发来的内容进行解析;如果该字段在htttp response header(响应头)里,指“后台向前端传递的数据的类型”。

如果要跨域就要解决两个关键问题,一个是浏览器不让你发请求(实质上是浏览器根据预检请求得到的结果,不让你发正式的请求),另一个是浏览器不让你收请求。

如何绕开同源策略(如何跨域)?

1. jsonp跨域

JSONP(JSON with Padding,带填充的json),是由Bob Ippolito在2005年提出的一种跨域手段。jsonp利用script标签从前端向后台请求数据。前端获得响应后,会在该script标签内执行一个回调函数。所以,jsonp跨域的两个关键点,一个是<script>标签,另一个是回调函数。

为什么是<script>标签呢?因为一些HTML标签是不受同源策略限制的,如scriptimg,而且这些标签会在浏览器渲染时,向指定的URL发送请求。(这里要注意哦,别有用心的人会利用这个特性发起CSRF攻击,详见:「每日一题」CSRF 是什么?)

为什么不用img标签呢?因为实际场景,前端不仅要发、收请求,还要对请求的结果进行下一步的处理。script标签内的内容可以作为js代码被浏览器执行,因此script非常符合我们的需求。

jsonp的另一个关键点就是回调函数了。利用<script>发起请求,返回内容是由后台生成的,因此在script标签内执行的代码只能是后台响应的数据。
这样一来…是不是有点尴尬,“后台大哥,我前端只能执行你发的数据,要不我等会代码写好了拷贝一份发你吧!”,后台大哥:“滚!”…
我们当然不能这样做,也不一定可行(作用域问题)。我们应该好好利用一个东西:函数!函数能把我们的业务代码囊括进去,一行代码便能执行数十行代码,还能利用闭包解决作用域的问题,简直太棒了!所以我们仅需要把回调函数告诉后台,让后台以函数的调用形式作为请求的响应内容返回就好了!至此,利用jsonp跨域的理论基础便建立起来了。

所以,利用jsonp发起一个跨域请求的具体步骤大致如下:

  1. 前端创建一个script标签,并设置该标签的src属性为我们期望请求api的url,设置type属性为"text/javascript"。
  2. 将该script标签添加至dom树。该标签加入dom树后,便会自动向src指定的url发送请求。
  3. 后台向前端响应的内容,应该是一段调用回调函数的代码,如:前端回调函数名(参数)
  4. 在前端声明一个回调函数,该函数在请求成功响应后执行。(注意,大部分情况都是要将该函数挂到全局对象上哦,因为script标签内的作用域是全局作用域)

jsonp跨域的一些缺点:

  1. 仅支持get请求。

1.1 jsonp跨域的原生实现:

/* vue前端 */
let script = document.createElement("script"), // 创建一个script标签
    jsonpCallbackName = "jsonpCallback", // 消除魔法字符串,统一后续的jsonp回调函数名称。
    context = this; // 记录当前的上下文。博主的这段代码实际是在vue中编写的,具体代码可见文末的demo。这条赋值语句可以忽略。

script.type = "text/javascript"; // 使该标签请求获得的数据可以被浏览器执行。
script.src = `http://localhost:3000/api/testGet?anything=${this.form.anything}&callback=${jsonpCallbackName}`; // 建议传一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数。
document.head.appendChild(script); // 将该标签添加至dom树,加入dom树后该标签会触发向src的请求。

// script标签请求成功后的回调函数。
window[jsonpCallbackName] = function (res) {
    document.head.removeChild(script);
    delete window[jsonpCallbackName];
    context.returnMsg = res;
};
/* node后台 */
router.get('/testGet', (ctx) => {
    console.log(`后台testGet接口被触发`);
    console.log(`接收到数据:${ctx.request.query.anything}`);
    ctx.response.status = 200;
    // 后台返回的内容必须为模拟执行回调函数的代码,即:回调函数名(参数)
    ctx.body = `${ctx.request.query.callback}({"status":"success"})`;
})

对于实现过程中的一些细节的解释:

  1. 为什么一定要给script标签设置type属性?

    type属性定义script元素包含或src引用的脚本语言。属性的值为MIME类型; 支持的MIME类型包括text/javascript, text/ecmascript, application/javascript, 和application/ecmascript。如果没有定义这个属性,脚本会被视作JavaScript。如果MIME类型不是JavaScript类型(上述支持的类型),则该元素所包含的内容会被当作数据块而不会被浏览器执行。 (摘自mdn)

  2. 为什么要将回调函数挂载到window上?
    • 因为script标签内的代码的执行环境是全局作用域,如果不将回调函数挂载到window上,script标签内的代码访问不到该回调函数。
  3. 为什么要在回调函数里删除该方法?
    • 为了避免内存泄漏。function也是会占用一定内存的。

可能会遇到的报错:

  1. Cross-Origin Read Blocking (CORB) blocked cross-origin response http://localhost:3000/api/testGet?name=test&callback=handleCallback with MIME type application/json. See https://www.chromestatus.com/feature/5629709824032768 for more details.

  • 出现原因:因为CORB策略(详见参考资料1)。后台返回的内容可能满足了CORB的保护规则,触发了CORB,因此前端无法获取到响应信息。
  • 解决方案:博主是初学时不懂后台要返回一个方法的调用(如:前端回调函数名(参数)),而是返回了一段json数据才导致的这个错误。

1.2 jquery版本的jsonp跨域

let jsonpCallbackName = "jsonpCallback",
    context = this;

window[jsonpCallbackName] = function (res) {
    delete window[jsonpCallbackName];
    context.returnMsg = res;
};

$.ajax({
    url: "http://localhost:3000/api/testGet",
    type: "get",
    dataType: "jsonp", // 预期服务器返回的数据类型。当值为"jsonp"时,会在url中自动添加"callback=?",其中?会被自动替换为jsonpCallback字段设置的函数名。
    jsonpCallback: jsonpCallbackName, // 回调函数的函数名。
    data: {
        anything: this.form.anything,
    },
});

2. iframe+form跨域

由于form表单在提交时不会出现跨域问题,因此可以利用form表单进行跨域。
ifame标签,主要用来避免页面刷新的问题,收到响应数据后直接将表单的数据显示到iframe标签中。
但是该方法的局限性比较大,后台传递回来的数据只在iframe中渲染,在iframe的父页面中无法获取。

/* 前端 */
let iframe = document.createElement("iframe"), // 首先创建一个用来发送数据的iframe.
	form = document.createElement("form"),
	node = document.createElement("input"),
	context = this,
	data = {
    	anything: this.form.anything,
	};

// 设置并添加iframe至dom树
iframe.name = "iframePost";
iframe.style.display = "none";
iframe.src = "http://localhost:8080";
iframe.addEventListener("load", function (res) {
    context.returnMsg = res;
    console.log(res);
});
document.body.appendChild(iframe);

// 设置并添加form至iframe
form.action = "http://localhost:3000/api/testPost";
form.target = iframe.name; // 在提交表单之后,在指定的iframe中显示响应信息
form.method = "post";
for (let prop in data) {
    node.name = prop;
    node.value = data[prop].toString();
    form.appendChild(node.cloneNode());
}
form.style.display = "none";
document.body.appendChild(form);
form.submit(); // 发送form
document.body.removeChild(form); // 表单提交后,就可以删除这个表单,不影响下次的数据发送.
};
/* 后台 */
router.post('/testPost', (ctx) => {
    ctx.response.status = 200;
    ctx.body = {
        status: "success"
    }
})

3. CORS(Cross-origin resource sharing,跨域资源共享)

cors是一种规范,这个规范规定了一些能够进行跨域的情况。
如果我们需要根据cors规范跨域,绝大多数情况下仅需要在前后端设置一下http请求的header。

关于cors规范,有些内容你必须了解

1. 简单请求和非简单请求

cors规范将请求分为了简单请求非简单请求。为什么要划分呢?因为有些http请求可能会对服务器数据产生副作用,划分请求便是为了阻止这些副作用。

这两个请求的最大区别,便是简单请求只需发送一个http请求,而非简单请求一共需要发送两个http请求。非简单请求首先发起一个预检请求,获知后台是否允许跨域请求,确认允许后才可以发起实际的HTTP请求。简单请求直接发送实际的HTTP请求。

满足以下所有条件即简单请求(***摘自mdn***)

  • 使用下列方法之一
    • GET
    • HEAD
    • POST
  • 除了被用户代理自动设置的首部字段(例如 Connection ,User-Agent)和在 Fetch 规范中定义为 禁用首部名称 的其他首部,允许人为设置的字段为 Fetch 规范定义的 对 CORS 安全的首部字段集合。该集合为:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (需要注意额外的限制)
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • Content-Type 的值仅限于下列三者之一:(牢记,容易出问题,我们常用的application/json并不包括在里面)
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded
  • 请求中的任意XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。
  • 请求中没有使用 ReadableStream 对象。

cors跨域该如何设置?

前端:

  1. 如果需要携带认证信息(包括cookie),需要设置xhr.withCredentials = true
  2. 根据自己传送的数据类型,设置一下contentType,如xhr.setRequestHeader("Content-Type", "application/json");

后台:

  1. Access-Control-Allow-Origin:设置允许访问资源的源,经常被设置为"*",注意如果携带了认证信息,该字段值不能设置为"*",需要设置为相应的uri。
  2. 如果携带认证信息(包括cookie),设置Access-Control-Allow-Credentials的值为true。非简单请求一般情况下也将该字段设置为true
  3. Access-Control-Allow-Headers:设置请求头中除一些标准的字段,额外允许携带的字段。比如Content-Type。多个字段用逗号隔开,如:'Content-Type, Content-Length, Authorization, Accept'
  4. Access-Control-Allow-Methods:设置前端可以使用哪些方法进行请求。可以设置为:'PUT, POST, GET, DELETE, OPTIONS'
    设置了这几个字段,基本就能实现请求的跨域了,其他字段可以根据需要另行设置。

与cors跨域相关的字段(仅做总结)

请求头的相关字段:
  • origin:发送请求的源的URI。
  • Access-Control-Request-Method:仅用于预检请求。将实际请求所使用的 HTTP 方法告诉服务器。
  • Access-Control-Request-Headers:仅用于预检请求。将实际请求所携带的首部字段告诉服务器。
响应头的相关字段:
  1. Access-Control-Allow-Origin,指定可以访问该资源的URI。“*”指任何人都可以访问;也可以是具体的uri,比如:https://developer.mozilla.org
    • 对于附带身份凭证的请求,或需要传递cookie的请求,该值不能使用"*"
  2. Access-Control-Allow-Credentials,表示是否可以将对请求的响应暴露给页面。
    • 如果前端要把cookie传递至后台,则后台必须将响应头中该字段的值设为true
  3. Access-Control-Allow-Methods,表示前端可以使用哪些方法进行请求。
  4. Access-Control-Allow-Headers,表示请求头中除一些标准的字段,额外允许携带的字段。
  5. Access-Control-Expose-Headers,扩展前端使用XMLHttpRequest对象的getResponseHeader()方法所能获取到的响应头信息。getResponseHeader()默认只能获取最基本的响应头:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma
  6. Access-Control-Max-Age,指定预检请求可以被缓存多少秒。

基于上述内容做的一个跨域模拟器

项目基于nodejs koa2和vue3.0实现(基本没用什么新特性…),这个模拟器可以用来:

  1. 观察跨域时的http报文…
  2. 直接看源码,了解上述跨域方法是如何使用的。

嗯嗯…就这些了…这也是我第一次用这个koa框架和vue3.0…喜欢的话帮忙点个Star呗~ 欢迎pr~ 项目地址:https://github.com/Michael-Zhang-Xian-Sen/cross-domain-simulation

参考资料

  1. 30 分钟理解 CORB 是什么
  2. Jsonp 维基百科
  3. 前端常见跨域解决方案(全)
  4. Remote JSON - JSONP
  5. 不要再问我跨域的问题了

延伸阅读

  1. w3c cors标准
  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值