聊聊跨域这件事

一、引言

跨域这件事可能是个老生常谈的问题,不光是面试中会高频问道,实际开发中也会经常遇到。今天就来好好探讨一下这个 “万恶” 的跨域吧。

为什么我的万恶要加上引号呢,因为并不是完全是坏处,正所谓存在即合理,它的存在必然有它的用处。

二、跨域

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 失败 主机不同

由此我们可以看出,只要协议主机端口有一个不相同就是跨域的

如果存在跨域情况,以下三种资源是不能访问的

  1. CookieLocalStorageIndexDB 无法读取。

  2. DOM 无法获得。禁止对不同源页面DOM进行操作。这里主要场景是iframe跨域的情况,不同域名的iframe是限制互相访问的。

  3. 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的请求,请求头中会包括OriginAccess-Control-Request-MethodAccess-Control-Request-Headers,还有一些浏览器自动代理的ConnectionUser-Agent等请求头,相当于告诉服务端,我的源(协议+主机+端口)是什么,我请求的方法和将要携带的请求头是哪些,你看看让不让我访问吧。
  • 服务端收到请求决定是否允许访问,如果可以访问,那么久返回一个200状态码,同时在响应Access-Control-Allow-Origin: http://foo.exampleAccess-Control-Allow-MethodsAccess-Control-Allow-HeadersAccess-Control-Max-Age等响应头,此时预检完成
  • 浏览器正式发送HTTP请求

3.2 简单请求

以下几种属于简单请求

  • 请求方法是GETPOSTHEAD
  • HTTP请求头只包含这几个AcceptAccept-LanguageContent-LanguageLast-Event-ID(浏览器还会代理ConnectionUser-Agent)
  • Content-Type只包括application/x-www-form-unlencodedmultipart/form-datatext/plain

除此之外是非简单请求,具体见CORS-MDN

四、 如何解决跨域问题

最常用的几种方式有(不具体展开, 每一条都可以从同源策略角度思考为什么可以解决跨域):

  • 后台允许跨域
  • 前端配置代理服务器,如通过webpack中的proxyTable
  • 通过nginx进行代理转发
  • 通过JSONP

4.1 后台允许跨域

服务端可以通过拦截器,为可以跨域的接口设置响应头,如Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-HeadersAccess-Control-Max-Age等,这些可以根据业务具体设置

具体看这篇文章CORS

4.2 前端配置代理服务器

前端可以通过配置webpack中的proxyTable来实现前端代理,这是一个简单例子

1
2
3
4
5
6
proxyTable: {
'/':{
target: "http://182.92.228.33:9004/",
changeOrigin: true
}
},

4.3 nginx转发

这个原理和3.2差不多,直接将请求重定向到真实服务器地址即可

1
2
3
4
5
http {
location /api/ {
proxy_pass http://localhost:9999;
}
}

4.4 JSONP

JSONP可以实现跨域,但是基本被淘汰了,利用跨域资源访问中插入内嵌资源不限制的特点,但是这个需要服务端配合,一般是这样

1
2
3
4
5
6
7
8
9
// 提前定义一个方法
<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请求,局限性太大