为什么会产生跨源
跨源产生的场景主要是在:如果A页面(通过B页面提供的API)获取B页面的资源,而A页面和B页面的源信息不同,那么便会产生跨源错误。
源信息包括:协议、域名、端口三部分,同源指的是源信息的协议、域名、端口都相同
这主要源于 跨源安全策略,简单说,如果两个页面的源信息不同,就不能通过 XHR 对象获取对方的信息。
需要注意的是,跨源策略是浏览器基于安全考虑做的限制,而非服务器端做了不允许跨源的限制。如果将浏览器设置为允许跨源请求,跨源策略随即失效(Safari浏览器开发菜单下拉列表中有“停用跨源限制”的选项,Chrome设置略复杂,请自行搜索解决)。
通过更改代码跳过浏览器跨源限制的方法很多,本文主要讨论常用的JSONP和CORS方法,完整代码已放在 Github 上 cross_origin_example。
跨源场景
跨源场景说明
url | 说明 | 是否允许通信 |
---|---|---|
http://www.domain.com/a.js http://www.domain.com/b.js http://www.domain.com/c/c.js |
同源,不同文件或路径 | 允许 |
http://www.domain.com:8080/a.js http://www.domain.com/b.js |
协议、域名相同,端口不同 | 不允许 |
http://www.domain.com/a.js http://www.domain111.com/b.js |
协议、端口相同,域名不同 | 不允许 |
http://www.domain.com/a.js http://x.domain.com/b.js http://domain.com/c.js |
协议、端口相同,域名不同(主域相同,子域不同) | 不允许 |
http://www.domain.com/a.js http://127.0.0.1/b.js |
协议、端口相同,域名不同(即便 www.domain.com 对应的ip地址为 127.0.0.1) | 不允许 |
http://www.domain.com/a.js https://www.domain.com/b.js |
域名、端口相同,协议不同 | 不允许 |
跨源实例
下面是通过写原生 XHR 对象,调用豆瓣图书的API
const xhr = new XMLHttpRequest(); |
直接使用上述代码会报下面的错误:
Failed to load https://api.douban.com/v2/book/6548683: |
该错误说明 https://api.douban.com
在响应头信息中没有设置 Access-Control-Allow-Origin
(CORS标准中定义的内容,后面讨论),这就不允许跨源请求。
- 此处我通过Node开启了本地服务,通过http://127.0.0.1:7070 这个地址进行的请求,如果直接将上述代码插入到 html 文件中,在本地直接访问也会报跨源错误,错误提示中 Origin 由 http://127.0.0.1:7070 变为
null
。- 不同浏览器的跨源安全策略实现方式有差异,上述代码在 Chrome(61.0.3163.100)浏览器下测试会报跨源错误,但 Safari(10.1.2)浏览器不会报跨源错误,可以正常返回数据。
JSONP实现跨源通信
JSONP(JSON with Padding) 实现跨源通信的原理是 Web 页面上调用外部 js 文件不受浏览器同源策略的限制,所以通过动态创建 script
便签,然后获取其内容,便实现了跨源通信,具体过程为:
- 首先在 DOM 中创建
script
标签; - 将
script
标签的src
属性值设置为请求API的地址(一般是一个绝对路径的url),这里的关键是 设置回调函数,并将其作为 url 的参数; - 服务端接收到请求后,通过该参数获得回调函数名,并将数据放在参数中将其返回;
- 获取数据后就可以通过提前设置的回调函数进一步处理数据。
JSONP实例
还是以调用豆瓣读书API为例,通过JSONP调用的代码如下:
// 创建script标签 |
执行上面代码,就可以跨源访问豆瓣API了。
优缺点
- 优点
- 兼容性很好,在古老的浏览器也能很好的运行;
- 简单,不管通过创建
script
标签获取数据,还是通过回调函数处理获取的数据,都简单明了; - 不需要服务端单独为某个域名做设置(对比CORS方法,下文说明),这在请求自己不受控的资源是特别受用(你总不能要求豆瓣或者某个提供查询天气状况的API提供商单独为你的域名做设置)。
- 缺点
- 只支持
get
; - 潜在的安全问题(大站提供的API可忽略);
- 稳定性存疑。毕竟,使用JSONP访问的资源往往都是别人家的,说不定哪天就关了或者代码有改动。
应用场景
主要是在访问不能按自己的需求随意更改的公共API时使用,比如查个天气什么的。
CORS实现跨源通信
CORS(Cross-Origin Resource Sharing,跨源资源共享)是 W3C 一项推荐标准,定义了在必须访问跨源资源时,浏览器和服务器应该如何通信。
其实现方式是,通过使用自定义的HTTP头信息让浏览器和服务器进行通信,进而确定是否允许跨源通信。
浏览器向服务器发送请求时会在 Request Headers 中自动添加请求源信息(Origin
字段),服务器端通过在 Response Headers 中添加 Access-Control-Allow-Origin
字段,指明允许那些源可以访问。经浏览器检查,如果请求源在服务器允许请求源列表中,则允许请求源从该服务器上的获取资源。
实际上,浏览器不会完全阻止跨源请求,如果源A要请求源B上的资源,浏览器会将请求到的数据放在 网络层 ,然后检查源A是否在源B允许的请求源列表上,如果没有,浏览器就会触发
XMLHttpRequest
的error
事件(此处就是跨源错误),同时阻止数据从网络层传送到源A的js代码中。
CORS实例
实现CORS方法需要服务源做设置,为了模拟这个场景通过Node搭建两个本地服务器,一个做请求源,一个做服务源。
需要对Node有基本了解,如何使用Node搭建本地服务器不在本文中讨论
服务源设置
// 引入http模块(http是node自带模块,安装node后自带,无需额外安装) |
上述代码中,关键是在 response headers 中添加 'Access-Control-Allow-Origin': 'http://127.0.0.1:7070',
,据此明确允许访问的源(列表)。如果是开放源,可以将 Access-Control-Allow-Origin
的值设为 '*'
,这样任何请求源都可以访问该服务源上的数据。
请求源设置
- 设置静态服务器
为方便起见,直接通过 http-server
模块搭建静态服务器,目的是通过指定源(此处为http://127.0.0.1:7070)发送请求。安装完 http-server
模块后通过下面命令进行启动
http-server -a 127.0.0.1 -p 7070 |
这里启动的静态服务器域名为 127.0.0.1
, 端口为 7070
,协议默认为 http
,静态目录就是当前运行命令所在目录,点击事先准备好的请求文件模拟跨域请求。
这里全局安装
http-server
模块后才能直接使用该命令,不过更推荐通过本地(局部)安装,然后通过 npm script 启动服务。
- 设置请求文件
实际发送请求的代码可以通过多种形式写,原生、axios 或者 jquery,随意,只要请求 url 是上文设置的服务源就好。下面是使用原生方式写的代码:
const xhr = new XMLHttpRequest(); |
优缺点
- 优点
- 前端不需要改动代码,就像访问同源资源一样;
- 没有对请求方法的限制。
- 缺点
- 存在兼容性问题:IE10以上。
- 最重要的是不能设置 cookie(cookie 遵循同源策略)。
应用场景
在服务源可以按照自己的需求更改(即在 response headers 中添加请求源信息)时使用,公司项目一般都是通过CORS方法实现跨源通信的。
待补充
涉及到简单请求和复杂请求,其中 preflight 请求是不带任何自定义请求头的,是浏览器自发行为。
通过 Nginx 反向代理
这是最好的方法,简单,前后端也不需要做额外更改。
参考资料
【1】How does Access-Control-Allow-Origin header work?
【2】关于跨域,你想知道的全在这里