0%

聊聊常见的跨域问题

跨域是前端工程中一个很常见的问题啦. 今天就来聊一聊跨域, 以及常见的处理方式和原理.

什么是跨域?

跨域实际上源于浏览器对javascript的一种安全限制(也被称之为同源策略). 默认情况下, 我们只能访问同一协议、同一域名、同一端口下的资源. 如今都是提倡前后分离的情况下, 前端更是需要调用大量后台接口的场景, 因此解决跨域的问题就摆在了面前.

产生跨域的原因

产生跨域的原因我们主要归为这两点:

  1. 浏览器安全限制(不能读取不同域、端口、协议下的内容)
  2. 使用的是XHR(XMLHttpRequest)请求

同源策略这个原因是众所周知了, 它作为一个安全策略, 的确有效预防了某些安全上的问题. 但同时又阻止了大量适合使用的跨域请求. 而 XHR 也受同源策略影响: 浏览器不允许 javascript 查找跨域文档的内容. 使用 XHR, 文档内容都是通过responseText属性暴露, 因此同源策略不允许 XHR 进行跨域请求.

解决跨域

跨域很多情况下都需要后端的配合, 因此主要先来谈谈前端的跨域方案.

JSONP

前文我们说过, 产生跨域的原因之一XHR请求, 但是script发出的请求类型(type)并不是xhr, 因此可以解决跨域的问题.

JSONP 由回调函数和数据组成的, 实现方式就是动态创建一个<script>标签, 然后设置src属性指向的跨域的URL(包涵请求参数). 来向服务端请求数据.

比如我们要查询小明的信息, 这时我们得知它的userID为 1150, 同时我们都知道GET请求可以通过url进行传参, 因此我们向服务器发起请求:

1
2
3
var script = document.createElement('script')
script.src = "https://www.example.com/users?user_id=1150"
document.body.appendChild(script)

在插入<script>标签到<body>后, 浏览器立马就去请求服务器的资源. 值得注意的是, 使用jsonp也需要服务端的配合. 因此必须通过某种方式来告知服务端, 我们正在通过<script>标签调用请求, 必须返回一个JSONP响应, 而不应该是普通JSON响应.

至于什么叫jsonp响应呢? 这里其实很好理解.. 假设后端发回来的是json格式的数据, 我们也用不了呀, 数据还是数据, 不会做任何变化.. 为了让浏览器可以在<script>标签里直接使用, 我们需要让服务端返回一段js代码 —— 用函数包装的json的形式(这也jsonp中”P(padding)”的含义). 这个函数名前后端可约定. 如下:

1
2
3
4
5
6
7
// 服务端返回 js 代码到 <script>里
userData({'naem':'小明','id':1150,'level':'中等'})

// 前端定义函数
function userData(data) {
// 当 jsonp 请求成功后, 将json传入函数并调用, 我们拿到 json 后就可以做一些其他的事
}

我们来拿B站为例. 打开chrome下的network, 上图就是jsonp的应用, 服务端返回的js脚本. 下图可以发现, 我们发出去的请求类型是script, 验证了前文所说的<script>不受同源策略影响的.

目前主流的类库都对jsonp进行了封装, 如JQuerygetJSONajax, 这里就不深入讲解了. 最后对jsonp总结一下:

jsonp实际上是一个非正式传输协议, 或者说是一种”投机取巧”的方式. 我们可以利用<script>的特性从而进行数据交互解决跨域的问题. 相对来说, 它也有一定的局限性: 只能应用在GET请求上, 除此之外还有安全性的问题 —— 只能用在我们信任的服务端, 因为你不能保证对方未来会给你传些什么…

跨域资源共享(CORS)

概述

说完了”不正规”的jsonp, 紧接着我们再说说原生的CORS规范. 我们先来看看官方的定义:

CORS(Cross-origin resource Sharing, 跨资源共享), 定义了访问跨域资源时, 浏览器和服务器应该如何沟通. 其背后主要思想就是使用自定义的HTTP头部来让浏览器与服务器进行沟通, 从而决定请求或相应是否成功, 还是应该失败.

目前主流的浏览器都已经对CORS有着良好的支持, 而IE8 ~ 9则还需要使用专用的XDomainRequest这里我们抛开不谈.

这个功能实际上是由浏览器自动完成的, 我们并不需要做什么额外的工作. 对于开发者来说, 也就需要了解一些安全细节的问题, 这一点我们放在后面讲.

两种请求

浏览器发送CORS请求时, 会将请求分为简单请求非简单请求.

在我们日常工作中, 常用的简单请求可以将其归为以下几点:

  • 使用的方法(Methods)为HEADGETPOST
  • 请求头无自定义头
  • Content-Type只能是以下几种
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

非简单请求:

  • PUT, Delete 方法的 ajax 请求
  • 发送 JSON 格式的 ajax 请求(比如post数据)
  • 带自定义头的 ajax 请求

如果是简单请求, 则会先执行, 后判断。执行的过程大致如下:

浏览器发起请求检测到是 CORS 请求, 然后添加一个origin字段(其中包含页面源信息: 协议、域名、端口) => 服务端收到后作相应的处理(对比origin, 服务端判断这个源是否接受)返回结果给浏览器 => 浏览器检查响应头是否允许跨域信息 => 允许, 那就当做没事发生. 不允许, 浏览器抛出相应的错误信息(值得一提的是, 这时状态码也还有可能是200).

非简单请求执行顺序又有些不同. 在发生 CORS 请求时, 浏览器预先发送一个option请求. 浏览器这种行为被称之为预检请求(Preflighted request). 其中包含如下的请求头:

  • origin: 同上,包含页面源信息.
  • Access-Control-Request-Methods: 请求方法
  • Access-Control-Request-Header: 自定义头部信息, 多个头部以逗号分隔(可选, 看请求时有没有定义请求头)

举个栗子, 我们用JQuery发送一段JSON格式的请求做演示:

1
2
3
4
5
6
7
8
9
10
11
var result;

$.ajax({
type : "post",
url: "https://www.example/api/rank",
contentType : "application/json;charset=utf-8",
data: JSON.stringify({name: "something"}),
success: function(json){
result = json;
}
});

这时请求头(Request Headers)信息如下:

1
2
3
4
5
6
7
8
9
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Access-Control-Request-Header: content-type
Access-Control-Request-Methods: POST
Connection:keep-alive
Host: https://www.example.com
origin: localhost:8080
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1

服务端接收到预检请求后, 判断是否允许这种类型的请求. 在响应头(Response Header)上返回如下头部与浏览器进行交流:

  • Access-Control-Allow-Origin: 服务端允许的源信息
  • Access-Control-Allow-Methods: 服务端允许的方法, 多个方法可以使用顿号分隔
  • Access-Control-Allow-Headrs: 服务端允许的头部, 多个头部可以使用顿号分隔

当预检请求被通过后, 我们原本想要发送的请求才会发送出去.

另外, 细心的你或许已经注意到了, 非简单请求这一来一回需要发送两次请求, 如果频率高的情况下岂不是很费性能又影响效率? 所幸的是HTTP协议新增(IE10+)了一个响应头用于缓存预检请求. 服务端在响应头添加如下字段:

1
Access-Control-Max-Age: 3600

这个响应头表示这个预检请求可以缓存多长时间, 单位为秒. 这里3600s = 1h, 也就是说一个小时内可以不用再发预检命令了.

默认情况下, 跨域请求是不带上 cookie 的. 前端需要将withCredentials属性设置为true, 同时还需要服务端设置Access-Control-Allow-Credentialstrue启动 cookie. 如果在发送 cookie 的时候, 浏览器检测到服务端响应头没有这个头部, 那么就会在控制台抛出一个错误.

另外, 还有一个值得注意的是. 服务端响应头设置了Access-Control-Allow-Origin: *的话, 是不能满足带 cookie 的跨域请求的. 因此有这种场景不能使用通配符, 需要全匹配字段.

CORS 总结

简单总结一下 CORS. CORS的出现也是为了解决跨域的问题. 只不过和JSONP不同, 它是纳入规范的一部分, 它几乎支持所有的类型的HTTP请求(JSONP只能使用GET). 唯一美中不足的也就是兼容性的问题, 因此可以使用JSONP作向下的兼容

事实上前端在 CORS 上并没有多少可操作的余地, 主要的还是浏览器来处理、服务端在设置, 但是并不代表我们就不需要了解这些知识啦.


嗯, 其他的跨域方法先挖个坑..

「请笔者喝杯奶茶鼓励一下」

欢迎关注我的其它发布渠道