有时候浏览器的 ajax 请求不能正常拉取到别的站点的数据,这是为什么呢?
同源策略
为了阻止一些低成本的跨站攻击,浏览器采取了同源策略
同源策略要求 3 个标准都要完全一致
- 协议
- 域名
- 端口
三个只要有任何一个不一致,那就认为不是同源,这时候的请求虽然能成功发到目标服务器,但并不能取得数据,因为数据被浏览器扣留了
举几个例子(http 默认地址是 80 端口,https 是 443,此时写与不写并无区别)
同源又称之为”同一个域”(不是域名)
跨域
但有的时候不得不跨站请求数据,比如前后端分离的时候,服务器地址往往和前端地址不一样
为了解决同源策略带来的限制,提出了跨域方案
常用的跨域方案有 JSONP 跨域和 CORS 跨域,还有一些其它的基于 iframe 的跨域,此处不介绍
JSONP 跨域
JSONP 跨域是在 IE 上的一种妥协
主要原理是利用 js 脚本可以任意引用,从而在 A 站与 B 站达成协议的情况下,B 站直接在 js 脚本里包藏数据,A 站直接引用这个脚本
但是 A 站引用后也不能直接看到脚本内容,所以通常采用的方式是 A 站预定义一个数据处理方法,B 站在脚本中直接调用该方法并传入数据
同时一般为了保持隐秘性,会让每次的数据处理方法名都不一样
一个 JSONP 实例如下
首先准备两台服务器模拟 A 站和 B 站
1 | var express = require('express'); |
1 | var express = require('express'); |
然后准备一个 html 用于显示 A 站的页面,请求 B 站的数据
1 |
|
html 里的内部 js 定义了一个随机函数,函数的效果是将字符串塞进显示区域内
同时定义了 script 标签取回后的动作,是执行取回的 js 后(默认动作),移除自身这个标签
再准备一个 friend.js 用于给 B 站返回
1 | window['{{callback}}']( |
该代码执行一个函数,传入指定的数据
这里是保留两个占位符,用服务器取得的请求参数(B 站代码的第 15 行)和数据库里取回的数据来填充
所以还要再写一个 json 用来临时顶替数据库保存数据
1 | [ |
先设置一下本地的 hosts 文件改变寻址,再启动两台服务器,访问 A 站的首页并点击按键发送请求,就可以看到原本不能取得的数据被打印在了 A 站首页的内容区域
CORS 跨域
CORS 跨域是利用 Access-Control-Allow-Origin 的请求头,来设置跨域允许
该请求头一般是在服务端处理请求,要回送响应的时候,设置在响应包中
该请求头的值可以是星号表示允许所有跨域,也可以是由分号分隔的域名列表
一个 CORS 实例如下
首先准备两台服务器模拟 A 站和 B 站
1 | var express = require('express'); |
1 | var express = require('express'); |
然后准备一个 html 用于显示 A 站的页面,请求 B 站的数据
1 |
|
html 里的内部 js 定义了一个 ajax,并规定当 ajax 返回数据时,填充数据到内容区域
再准备一个 friend.json 用于给 B 站返回
1 | [ |
先设置一下本地的 hosts 文件改变寻址,再启动两台服务器,访问 A 站的首页并点击按键发送请求,就可以看到数据成功填充,并且控制台的请求包有 ‘Access-Control-Allow-Origin’ 字段
简单请求和非简单请求
但是刚刚的 CORS 跨域只是最简单的 CORS 跨域,其实 CORS 跨域还有很多限制
浏览器将CORS请求分成两类:简单请求和非简单请求
只要同时满足以下两大条件,就属于简单请求
请求方法是以下三种方法之一
- HEAD
- GET
- POST
刚才的例子中我们采用的就是 GET 方法
HTTP 请求包的头部不含有除以下字段外的其它字段
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type,只能是空或以下三种之一
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
可见 application/json 请求是不行的
这是为了兼容表单,因为历史上表单一直可以发出跨域请求
ajax 的跨域设计就是,只要表单可以发,ajax 就可以直接发
说是上面那么说,其实实际开发的时候谁记得住(
只要记得把 Chrome 开发者工具的 Disable cache 开了,然后看看请求有没有发个 options 预检就行了
如果后端没有配置的话,任何非简单请求都不会被放行
简单请求
对于简单请求,浏览器会在请求包中增加一个 Origin 字段,然后直接发出 CORS 请求
如果 Origin 指定的源不在许可范围内,服务器就会返回一个正常的 HTTP 响应,浏览器发现响应包中没有 Access-Control-Allow-Origin 字段,就会抛出一个错误
如果成功返回了,浏览器得到的响应可能会多出几个字段
1 | Access-Control-Allow-Origin: http://enatsu.com |
其中前三个都是和 CORS 高度相关的头部,且第一个和第四个是一定会出现的头部
Access-Control-Allow-Origin
如果 CORS 成功返回,则该字段只能是两种情况之一
- 星号,表示允许所有跨域
- 与请求中 Origin 字段相同
Access-Control-Allow-Credentials
浏览器在 CORS 请求中默认是不发送 Cookie 的
如果服务器允许该源的 CORS 请求发送 Cookie,将该字段设为 true 即可
该字段不能设为 false,如果想设为 false,可以直接删除该字段
Access-Control-Expose-Headers
浏览器在 CORS 请求中,响应包的默认头部一般只有以下 6 个基本字段
- Cache-Control
- Content-Language
- Content-Type
- Expires
- Last-Modified
- Pragma
但是看到 Etag 什么的也别惊讶,毕竟实际开发不能认死理对吧
如果需要让浏览器拿到其它字段,服务器必须设置该字段的值
比如上例中设置该字段的值为 Authorization,则浏览器就可以从响应头部中取得字段 Authorization 的值
Content-Type
该字段取值取决于返回的数据类型,不解释了
withCredentials
刚才说到浏览器在 CORS 请求中默认是不发送 Cookie 的,如果有这个需求,应该由服务器设置 Access-Control-Allow-Credentials 为 true
但其实这样还不够,原则上来说,不止服务端要同意,前端也要主动设置 XMLHTTPRequest 的 withCredentials 为 true,否则即使服务器同意,前端也不会发送 Cookie
但是呢,还是有的浏览器搞特殊,不设也会发,所以刚才只是说原则上是这样,2333
除了这两个要求之外,当有跨域 Cookie 需求的时候,Access-Control-Allow-Origin 的值也不能是星号,必须是具体值
非简单请求
当你请求动词是 PUT、DELETE 之类的时候,或者 Content-Type 是 application/json 的时候,就是非简单请求
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)
预检请求
浏览器会先向目标服务器发送一个 OPTIONS 请求,询问服务器当前域名是否被许可,以及允许哪些 HTTP 动词和头部字段
预检请求除了请求方法是 OPTIONS 之外,还包含三个关键字段
- Origin,表示请求源
- Access-Control-Request-Method,表示浏览器会用到哪些 HTTP 请求动词
- Access-Control-Request-Headers,表示浏览器会发送哪些简单请求之外的头部字段,用逗号隔开
上述前两个字段都是必须给出的
预检请求的响应
服务器确认了上述三个字段的值之后,就会做出响应
如果服务器拒绝响应,会返回一个正常的 HTTP 响应,但是没有任何头部字段,浏览器就会认为服务器拒绝了跨域预检申请,此时会触发 ajax 或 XMLHttpRequest 的 error 事件
如果服务器同意了预检请求,就会在返回的响应包中加入一些特殊的头部字段,一般有以下 4 种
Access-Control-Allow-Methods,指示服务器接受哪些 HTTP 方法,必须
Access-Control-Allow-Headers,指示服务器允许的头部字段
如果请求中包含 Access-Control-Request-Headers 字段,则响应中该字段是必须的
Access-Control-Allow-Credentials,指示是否允许跨域 Cookie,与简单请求中相同
Access-Control-Max-Age,指示当前预检请求的有效期,单位为秒,在有效期内不需要再次发出第二次预检请求
预检请求之后
即便通过了预检,浏览器和服务器之间的交互还是存在间隙,不能像真同源一样通信
具体表现在浏览器会在每条请求中都加入 Origin 字段,服务器则会在每条响应中都加入 Access-Control-Allow-Origin 字段
总结
CORS 跨域,功能更强大,但是细节更复杂,需要开发者盯紧开发者工具,并且和后端的同学协调好(但是也便于甩锅2333
JSONP 跨域只支持 GET 方法,但是支持旧时代的浏览器(比如 IE),以及向某些达成合意但是因为某些原因不支持 CORS 的网站发送跨域请求
参考链接
感谢阅读