一、引言
跨域这件事可能是个老生常谈的问题,不光是面试中会高频问道,实际开发中也会经常遇到。今天就来好好探讨一下这个 “万恶” 的跨域吧。
为什么我的万恶要加上引号呢,因为并不是完全是坏处,正所谓存在即合理,它的存在必然有它的用处。
二、跨域
2.1 跨域是什么
谈到跨域,我理解的是一种WEB规范,一种标准,个人认为应该叫做
跨源
,后面会解释因为啥
而最常遇到的便是浏览器跨域,因为浏览器在开发时候遵循了同源策略
这种WEB规范,所以浏览器会存在跨域。
最常见的跨域是主机或者端口不同,也就是你本地启动项目,但是后端服务在服务器上,由于两台电脑IP
不同,因此存在跨域
2.2 什么是同源策略
同源的概念(引自MDN)
如果两个 URL 的 协议、端口 (如果有指定的话)和 主机 都相同的话,则这两个 URL 是同源。这个方案也被称为
协议/主机/端口元组
,或者直接是元组
。如果三者有一个不相同,就会存在跨域问题(IE没有完全遵循这个同源策略)
引申:socket套接字按照这种理解应该也属于一种元组
下表给出了与 URL http://store.company.com/dir/page.html
的源进行对比的示例
URL | 结果 | 原因 |
---|---|---|
http://store.company.com/dir2/other.html | 同源 | 只有路径不同 |
http://store.company.com/dir/inner/another.html | 同源 | 只有路径不同 |
https://store.company.com/secure.html | 失败 | 协议不同 |
http://store.company.com:81/dir/etc.html | 失败 | 端口不同 ( http:// 默认端口是80) |
http://news.company.com/dir/other.html | 失败 | 主机不同 |
由此我们可以看出,只要协议
,主机
,端口
有一个不相同就是跨域的
如果存在跨域情况,以下三种资源是不能访问的
-
Cookie
、LocalStorage
和IndexDB
无法读取。 -
DOM
无法获得。禁止对不同源页面DOM
进行操作。这里主要场景是iframe
跨域的情况,不同域名的iframe
是限制互相访问的。 -
AJAX
请求不能发送
2.3 为什么要存在同源策略
正如我前言中说的,存在即合理,为什么同源策略有存在的必要呢,同源策略的存在是为了WEB安全,因为跨域下不可以访问2.2中说到的资源,可以减少被攻击的风险,或者说增加被攻击的成本。
Cookie中可能存在用户登录信息等敏感信息,如果跨域情况下可以获取cookie,那么攻击者有可能能直接通过这个cookie登录网站或者解析出敏感信息,而且早期有些功能如记住密码就是通过cookie实现的,因此同源策略在一定程度上能起到保护作用
DOM更危险,如果可以跨域访问DOM, 可以直接嵌入一个iframe
,通过DOM操作在iframe
中插入一段js脚本来达到攻击的目的
三、预检请求
2021/9/13日更新
水群时有人问了一个问题,为什么代码只发了一个http请求,却在浏览器控制台看到了两个请求?
知道这是浏览器行为,于是解释说这是一个预检请求
,浏览器如果检测到这个请求不是简单请求
,就会发送一个OPTION
请求,用来判断这个请求后台是否允许,但是当追问道什么事简单请求时,却发现自己没有深究过,特地去查阅了阮一峰的博客 和 MDN,完善一下这部分
3.1 预检过程
如果是非简单请求
(3.2介绍),在正式发送请求之前,浏览器会发送一次HTTP查询请求,被称为预检
(preflight)。
这个过程其实很好理解,我举一个小情侣谈恋爱的例子
小王和小红同学最近在谈恋爱,这一天小王同学想拉进一下两个人的关系,就想牵小红的手,然而小王同学不知道小红同学让不让牵。怎么办呢?他想到了一个好办法(预检),我先碰一下她的手,看看她的反应(预检过程),如果不拒绝就可以大胆的牵手了,如果小红避开了,那就算了吧,发展的太快了(跨域了)
引用mdn预检过程的图
过程文字版
- 浏览器判断当前请求是
简单请求
还是非简单请求
- 如果是非简单请求,则浏览器会发送一个OPTIONS的请求,请求头中会包括
Origin
,Access-Control-Request-Method
,Access-Control-Request-Headers
,还有一些浏览器自动代理的Connection
和User-Agent
等请求头,相当于告诉服务端,我的源(协议
+主机
+端口
)是什么,我请求的方法和将要携带的请求头是哪些,你看看让不让我访问吧。 - 服务端收到请求决定是否允许访问,如果可以访问,那么久返回一个200状态码,同时在响应
Access-Control-Allow-Origin: http://foo.example
,Access-Control-Allow-Methods
,Access-Control-Allow-Headers
,Access-Control-Max-Age
等响应头,此时预检完成 - 浏览器正式发送HTTP请求
3.2 简单请求
以下几种属于简单请求
- 请求方法是
GET
,POST
,HEAD
HTTP
请求头只包含这几个Accept
,Accept-Language
,Content-Language
,Last-Event-ID
(浏览器还会代理Connection
和User-Agent
)Content-Type
只包括application/x-www-form-unlencoded
、multipart/form-data
、text/plain
除此之外是非简单请求,具体见CORS-MDN
四、 如何解决跨域问题
最常用的几种方式有(不具体展开, 每一条都可以从同源策略角度思考为什么可以解决跨域):
- 后台允许跨域
- 前端配置代理服务器,如通过webpack中的proxyTable
- 通过nginx进行代理转发
- 通过JSONP
4.1 后台允许跨域
服务端可以通过拦截器,为可以跨域的接口设置响应头,如Access-Control-Allow-Origin
,Access-Control-Allow-Methods
,Access-Control-Allow-Headers
,Access-Control-Max-Age
等,这些可以根据业务具体设置
具体看这篇文章CORS
4.2 前端配置代理服务器
前端可以通过配置webpack
中的proxyTable
来实现前端代理,这是一个简单例子
proxyTable: {
'/':{
target: "http://182.92.228.33:9004/",
changeOrigin: true
}
},
4.3 nginx转发
这个原理和3.2差不多,直接将请求重定向到真实服务器地址即可
http {
location /api/ {
proxy_pass http://localhost:9999;
}
}
4.4 JSONP
JSONP可以实现跨域,但是基本被淘汰了,利用跨域资源访问中插入内嵌资源不限制的特点,但是这个需要服务端配合,一般是这样
// 提前定义一个方法
<script>
function test(res) {
console.log(res);
}
</script>
//这个接口应该返回一个这种‘test("{name: '123', age: '12'}")’字符串
<script src="http://192.168.1.5:9999/api/jsonp"></script>
这样接口返回后会直接调用已经定义好的test方法,但是有很多弊端
- 首先是这样写代码需要后台配合,不够优雅且不好维护
- 其次JSONP只能发送GET请求,局限性太大