必备知识
- 同源策略(Same origin policy),浏览器的一个安全策略,现代浏览器大部分都实现了该策略。
- 同源指:域名、协议、端口相同。
- 禁止使用 XHR 对象向不同源的服务器地址发起 HTTP 请求。
- 禁止对不同源页面 DOM 进行操作。这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的。
- 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标签是不受同源策略限制的,如script
、img
,而且这些标签会在浏览器渲染时,向指定的URL发送请求。(这里要注意哦,别有用心的人会利用这个特性发起CSRF攻击,详见:「每日一题」CSRF 是什么?)
为什么不用img
标签呢?因为实际场景,前端不仅要发、收请求,还要对请求的结果进行下一步的处理。script标签内的内容可以作为js代码被浏览器执行,因此script非常符合我们的需求。
jsonp的另一个关键点就是回调函数了。利用<script>
发起请求,返回内容是由后台生成的,因此在script
标签内执行的代码只能是后台响应的数据。
这样一来…是不是有点尴尬,“后台大哥,我前端只能执行你发的数据,要不我等会代码写好了拷贝一份发你吧!”,后台大哥:“滚!”…
我们当然不能这样做,也不一定可行(作用域问题)。我们应该好好利用一个东西:函数!函数能把我们的业务代码囊括进去,一行代码便能执行数十行代码,还能利用闭包解决作用域的问题,简直太棒了!所以我们仅需要把回调函数告诉后台,让后台以函数的调用形式作为请求的响应内容返回就好了!至此,利用jsonp跨域的理论基础便建立起来了。
所以,利用jsonp发起一个跨域请求的具体步骤大致如下:
- 前端创建一个
script
标签,并设置该标签的src
属性为我们期望请求api的url,设置type
属性为"text/javascript"。 - 将该
script
标签添加至dom树。该标签加入dom树后,便会自动向src
指定的url发送请求。 - 后台向前端响应的内容,应该是一段调用回调函数的代码,如:
前端回调函数名(参数)
。 - 在前端声明一个回调函数,该函数在请求成功响应后执行。(注意,大部分情况都是要将该函数挂到全局对象上哦,因为script标签内的作用域是全局作用域)
jsonp跨域的一些缺点:
- 仅支持
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"})`;
})
对于实现过程中的一些细节的解释:
- 为什么一定要给
script
标签设置type
属性?type
属性定义script元素包含或src引用的脚本语言。属性的值为MIME类型; 支持的MIME类型包括text/javascript, text/ecmascript, application/javascript, 和application/ecmascript。如果没有定义这个属性,脚本会被视作JavaScript。如果MIME类型不是JavaScript类型(上述支持的类型),则该元素所包含的内容会被当作数据块而不会被浏览器执行。 (摘自mdn) - 为什么要将回调函数挂载到window上?
- 因为
script
标签内的代码的执行环境是全局作用域,如果不将回调函数挂载到window上,script标签内的代码访问不到该回调函数。
- 因为
- 为什么要在回调函数里删除该方法?
- 为了避免内存泄漏。function也是会占用一定内存的。
可能会遇到的报错:
-
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跨域该如何设置?
前端:
- 如果需要携带认证信息(包括cookie),需要设置
xhr.withCredentials = true
。 - 根据自己传送的数据类型,设置一下
contentType
,如xhr.setRequestHeader("Content-Type", "application/json");
后台:
Access-Control-Allow-Origin
:设置允许访问资源的源,经常被设置为"*"
,注意如果携带了认证信息,该字段值不能设置为"*"
,需要设置为相应的uri。- 如果携带认证信息(包括cookie),设置
Access-Control-Allow-Credentials
的值为true
。非简单请求一般情况下也将该字段设置为true
。 Access-Control-Allow-Headers
:设置请求头中除一些标准的字段,额外允许携带的字段。比如Content-Type
。多个字段用逗号隔开,如:'Content-Type, Content-Length, Authorization, Accept'
Access-Control-Allow-Methods
:设置前端可以使用哪些方法进行请求。可以设置为:'PUT, POST, GET, DELETE, OPTIONS'
设置了这几个字段,基本就能实现请求的跨域了,其他字段可以根据需要另行设置。
与cors跨域相关的字段(仅做总结)
请求头的相关字段:
origin
:发送请求的源的URI。Access-Control-Request-Method
:仅用于预检请求。将实际请求所使用的 HTTP 方法告诉服务器。Access-Control-Request-Headers
:仅用于预检请求。将实际请求所携带的首部字段告诉服务器。
响应头的相关字段:
Access-Control-Allow-Origin
,指定可以访问该资源的URI。“*”指任何人都可以访问;也可以是具体的uri,比如:https://developer.mozilla.org
。- 对于附带身份凭证的请求,或需要传递cookie的请求,该值不能使用
"*"
- 对于附带身份凭证的请求,或需要传递cookie的请求,该值不能使用
Access-Control-Allow-Credentials
,表示是否可以将对请求的响应暴露给页面。- 如果前端要把cookie传递至后台,则后台必须将响应头中该字段的值设为
true
- 如果前端要把cookie传递至后台,则后台必须将响应头中该字段的值设为
Access-Control-Allow-Methods
,表示前端可以使用哪些方法进行请求。Access-Control-Allow-Headers
,表示请求头中除一些标准的字段,额外允许携带的字段。Access-Control-Expose-Headers
,扩展前端使用XMLHttpRequest
对象的getResponseHeader()
方法所能获取到的响应头信息。getResponseHeader()
默认只能获取最基本的响应头:Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
Access-Control-Max-Age
,指定预检请求可以被缓存多少秒。
基于上述内容做的一个跨域模拟器
项目基于nodejs koa2和vue3.0实现(基本没用什么新特性…),这个模拟器可以用来:
- 观察跨域时的http报文…
- 直接看源码,了解上述跨域方法是如何使用的。
嗯嗯…就这些了…这也是我第一次用这个koa框架和vue3.0…喜欢的话帮忙点个Star呗~ 欢迎pr~ 项目地址:https://github.com/Michael-Zhang-Xian-Sen/cross-domain-simulation