一.什么是跨域?
跨域的产生来源于现代浏览器所通用的同源策略
,指只有在url的
- 协议名
- 域名
- 端口名
均一样的情况下,才允许访问相同的cookie,localStorage,以及访问页面的DOM或者发送Ajax请求。若在不同源的情况下访问,就称为跨域
。
例如以下为同源:
http://www.example.com:8080/index.html
http://www.example.com:8080/home.html
复制代码
以下为跨域:
http://www.example.com:8080/index.html
http://www3.example.com:8080/index.html
复制代码
注意⚠️:
但是有两种情况:http
默认的端口号为80
,https
默认端口号为443
。
http://www.example.com:80 === http://www.example.com
https://www.example.com:443 === https://www.example.com
复制代码
二.为什么浏览器会禁止跨域?
-
首先,跨域只存在于浏览器端。浏览器它为
web
提供了访问入口,并且访问的方式很简单,在地址栏输入要访问的地址或者点击某个链接就可以了,正是这种开放的形态,所以我们需要对它有所限制。 -
所以同源策略它的产生主要是为了保证用户信息的安全,防止恶意的网站窃取数据。分为两种:
Ajax
同源策略与DOM
同源策略:Ajax
同源策略它主要做了这两种限制:1.不同源页面不能获取cookie
;2.不同源页面不能发起Ajax
请求。我认为它是防止CSRF
攻击的一种方式吧。因为我们知道cookie
这个东西它主要是为了解决浏览器与服务器会话状态的问题,它本质上是存储在浏览器或本地文件中一个小小的文本文件,那么它里面一般都会存储了用户的一些信息,包括隐私信息。如果没有Ajax
同源策略,恶意网站只需要一段脚本就可以获取你的cookie
,从而冒充你的身份去给其它网站发送恶意的请求。DOM
同源策略也一样,它限制了不同源页面不能获取DOM
。例如一个假的网站利用iframe
嵌套了一个银行网站mybank.com,并把宽高或者其它部分调整的和原银行网站一样,仅仅只是地址栏上的域名不同,若是用户没有注意的话就以为这个是个真的网站。如果这时候用户在里面输入了账号密码,如果没有同源策略,那么这个恶意网站就可以获取到银行网站中的DOM
,也就能拿到用户的输入内容以此来达到窃取用户信息的攻击。
同源策略它算是浏览器安全的第一层屏障吧,因为就像CSRF
攻击,它只能限制不同源页面cookie
的获取,但是攻击者还可能通过其它的方式来达到攻击效果。
(注,上面提到的iframe
限制DOM
查询,案例如下)
// HTML
<iframe name="yinhang" src="https://juejin.cn/post/www.yinhang.com"></iframe>
// JS
// 由于没有同源策略的限制,钓鱼网站可以直接拿到别的网站的Dom
const iframe = window.frames['yinhang']
const node = iframe.document.getElementById('你输入账号密码的Input')
console.log(`拿到了这个${node},我还拿不到你刚刚输入的账号密码吗`)
复制代码
三.解决方案
这里我只写一下我了解的,以及比较常用的。
3.1 jsonp跨域
基本原理:主要就是利用 script
标签的src
属性没有跨域的限制,通过指向一个需要访问的地址,由服务端返回一个预先定义好的 Javascript
函数的调用,并且将服务器数据以该函数参数的形式传递过来,此方法需要前后端配合完成。
执行过程:
- 前端定义一个解析函数(如:
jsonpCallback = function (res) {}
) - 通过
params
的形式包装script
标签的请求参数,并且声明执行函数(如cb=jsonpCallback
) - 后端获取到前端声明的执行函数(
jsonpCallback
),并以带上参数且调用执行函数的方式传递给前端 - 前端在
script
标签返回资源的时候就会去执行jsonpCallback
并通过回调函数的方式拿到数据了
「JSONP优点」
- 兼容性比较好,在一些古老的浏览器中都可以运行。
「JSONP缺点」
- 只能进行
GET
请求
「与AJAX对比」
- JSONP和AJAX相同,都是客户端向服务器发送请求,从服务器获取数据的方式。但是AJAX属于同源策略,JSONP属于非同源策略(跨域请求)
代码实现:
(具体可以看这篇文章:JSONP原理及实现)
3.1.1 封装一个JSONP方法
function JSONP({
url,
params = {},
callbackKey = 'cb',
callback
}) {
// 定义本地的唯一callbackId,若是没有的话则初始化为1
JSONP.callbackId = JSONP.callbackId || 1;
let callbackId = JSONP.callbackId;
// 把要执行的回调加入到JSON对象中,避免污染window
JSONP.callbacks = JSONP.callbacks || [];
JSONP.callbacks[callbackId] = callback;
// 把这个名称加入到参数中: 'cb=JSONP.callbacks[1]'
params[callbackKey] = `JSONP.callbacks[${callbackId}]`;
// 得到'id=1&cb=JSONP.callbacks[1]'
const paramString = Object.keys(params).map(key => {
return `${key}=${encodeURIComponent(params[key])}`
}).join('&')
// 创建 script 标签
const script = document.createElement('script');
script.setAttribute('src', `${url}?${paramString}`);
document.body.appendChild(script);
// id自增,保证唯一
JSONP.callbackId++;
}
复制代码
具体实现,看上面文章。
3.2跨域资源共享CORS
跨域资源共享(CORS
)是一种机制,是W3C标准。它允许浏览器向跨源服务器,发出XMLHttpRequest
或Fetch
请求。并且整个CORS
通信过程都是浏览器自动完成的,不需要用户参与。
而使用这种跨域资源共享
的前提是,浏览器必须支持这个功能,并且服务器端也必须同意这种"跨域"
请求。因此实现CORS
的关键是服务器需要服务器。通常是有以下几个配置:
- Access-Control-Allow-Origin
- Access-Control-Allow-Methods
- Access-Control-Allow-Headers
- Access-Control-Allow-Credentials
- Access-Control-Max-Age
具体可看:developer.mozilla.org/zh-CN/docs/…
过程分析:
- 浏览器先根据同源策略对前端页面和后台交互地址做匹配,若同源,则直接发送数据请求;若不同源,则发送跨域请求。
- 服务器收到浏览器跨域请求后,根据自身配置返回对应文件头。若未配置过任何允许跨域,则文件头里不包含
Access-Control-Allow-origin
字段,若配置过域名,则返回Access-Control-Allow-origin + 对应配置规则里的域名的方式
。 - 浏览器根据接受到的 响应头里的
Access-Control-Allow-origin
字段做匹配,若无该字段,说明不允许跨域,从而抛出一个错误;若有该字段,则对字段内容和当前域名做比对,如果同源,则说明可以跨域,浏览器接受该响应;若不同源,则说明该域名不可跨域,浏览器不接受该响应,并抛出一个错误。
另外在CORS
中有简单请求
和非简单请求
,简单请求是不会触发CORS
的预检请求的,而非简单请求会。
“需预检的请求”
要求必须首先使用 OPTIONS
方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。”预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
3.2.1 CORS的哪些是简单请求?
简单请求不会触发CORS
的预检请求,若请求满足所有下述条件,则该请求可视为“简单请求”:
简单回答:
- 只能使用
GET
、HEAD
、POST
方法。使用POST
方法向服务器发送数据时,Content-Type
只能使用application/x-www-form-urlencoded
、multipart/form-data
或text/plain
编码格式。 - 请求时不能使用自定义的
HTTP Headers
详细回答:
-
(一) 使用下列方法之一
GET
HEAD
POST
-
(二) 人为设置以下集合外的请求头
Accept
Accept-Language
Content-Language
Content-Type
(但是有限制)DPR
Downlink
Save-Data
Viewport-Width
Width
-
(三)
Content-Type
的值仅限于下面的三者之一text/plain
multipart/form-data
application/x-www-form-urlencoded
-
请求中的任意
XMLHttpRequestUpload
对象均没有注册任何事件监听器;XMLHttpRequestUpload
对象可以使用XMLHttpRequest.upload
属性访问。 -
请求中没有使用
ReadableStream
对象。
除了上面这些请求外,都是非简单请求。
3.2.2 CORS的预检请求具体是怎样的?
若是跨域的非简单请求的话,浏览器会首先向服务器发送一个预检请求,以获知服务器是否允许该实际请求。
整个过程大概是:
- 浏览器给服务器发送一个
OPTIONS
方法的请求,该请求会携带下面两个首部字段:Access-Control-Request-Method
: 实际请求要用到的方法Access-Control-Request-Headers
: 实际请求会携带哪些首部字段
- 若是服务器接受后续请求,则这次预请求的响应体中会携带下面的一些字段:
Access-Control-Allow-Methods<span> </span>
: 服务器允许使用的方法Access-Control-Allow-Origin
: 服务器允许访问的域名Access-Control-Allow-Headers
: 服务器允许的首部字段Access-Control-Max-Age
: 该响应的有效时间(s),在有效时间内浏览器无需再为同一个请求发送预检请求
- 预检请求完毕之后,再发送实际请求
这里有两点要注意:
一:
Access-Control-Request-Method
没有s
Access-Control-Allow-Methods
有s
二:
关于Access-Control-Max-Age
,浏览器自身也有维护一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效,而是以最大有效时间为主。
3.2.3 CORS附带身份凭证
对于跨域 XMLHttpRequest
或 Fetch 请求,浏览器不会发送身份凭证信息。如果要发送凭证信息,需要设置 XMLHttpRequest<span> </span>
的某个特殊标志位。
例如我们想要在跨域请求中带上cookie
,需要满足3个条件:
- web(浏览器)请求设置
withCredentials
为true
- 服务器设置首部字段
Access-Control-Allow-Credentials
为true
- 服务器的
Access-Control-Allow-Origin
不能为*
3.2.4 如何减少CORS预请求的次数?
方案一:发出简单请求(这不是废话吗…)
方案二:服务端设置Access-Control-Max-Age
字段,在有效时间内浏览器无需再为同一个请求发送预检请求。但是它有局限性:只能为同一个请求缓存,无法针对整个域或者模糊匹配 URL 做缓存。